diff --git a/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/DynamoConversationStore.cs b/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/DynamoConversationStore.cs index 5c84f852..35a739a8 100644 --- a/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/DynamoConversationStore.cs +++ b/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/DynamoConversationStore.cs @@ -68,9 +68,9 @@ public DynamoConversationStore(IDynamoClient dynamoClient, DynamoConversationSto [_configuration.PartitionKeyName] = _configuration.ConversationPkFormat(conversationId), [_configuration.SortKeyName] = _configuration.ConversationSkValue, [IdAttribute] = conversationId, - [CreatedAtAttribute] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() }, - [UpdatedAtAttribute] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() }, - [MessageCountAttribute] = new AttributeValue { N = "0" } + [CreatedAtAttribute] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString()), + [UpdatedAtAttribute] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString()), + [MessageCountAttribute] = AttributeValue.Number("0") }; if (metadata is not null) @@ -81,7 +81,7 @@ public DynamoConversationStore(IDynamoClient dynamoClient, DynamoConversationSto if (_configuration.DefaultTtl.HasValue) { var expiresAt = now.Add(_configuration.DefaultTtl.Value).ToUnixTimeSeconds(); - item[_configuration.TtlAttributeName] = new AttributeValue { N = expiresAt.ToString() }; + item[_configuration.TtlAttributeName] = AttributeValue.Number(expiresAt.ToString()); } var request = new PutItemRequest @@ -261,9 +261,9 @@ private async Task>> AddMessagesInter [_configuration.SortKeyName] = _configuration.MessageSkFormat(conversationId, sequenceNumber), [IdAttribute] = messageId, [ConversationIdAttribute] = conversationId, - [SequenceNumberAttribute] = new AttributeValue { N = sequenceNumber.ToString() }, + [SequenceNumberAttribute] = AttributeValue.Number(sequenceNumber.ToString()), [RoleAttribute] = role.ToString(), - [CreatedAtAttribute] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() } + [CreatedAtAttribute] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString()) }; // Serialize content blocks @@ -275,14 +275,14 @@ private async Task>> AddMessagesInter return serialized.Errors; contentList.Add(serialized.Value); } - messageItem[ContentAttribute] = new AttributeValue { L = contentList }; + messageItem[ContentAttribute] = AttributeValue.FromList(contentList); // Add token usage if present if (tokenUsage is not null) { - messageItem[InputTokensAttribute] = new AttributeValue { N = tokenUsage.InputTokens.ToString() }; - messageItem[OutputTokensAttribute] = new AttributeValue { N = tokenUsage.OutputTokens.ToString() }; - messageItem[TokensAttribute] = new AttributeValue { N = tokenUsage.TotalTokens.ToString() }; + messageItem[InputTokensAttribute] = AttributeValue.Number(tokenUsage.InputTokens.ToString()); + messageItem[OutputTokensAttribute] = AttributeValue.Number(tokenUsage.OutputTokens.ToString()); + messageItem[TokensAttribute] = AttributeValue.Number(tokenUsage.TotalTokens.ToString()); totalInputTokens += tokenUsage.InputTokens; totalOutputTokens += tokenUsage.OutputTokens; @@ -298,18 +298,18 @@ private async Task>> AddMessagesInter var tagValues = new List(); foreach (var value in kvp.Value) { - tagValues.Add(new AttributeValue { S = value }); + tagValues.Add(AttributeValue.String(value)); } - tagsMap[kvp.Key] = new AttributeValue { L = tagValues }; + tagsMap[kvp.Key] = AttributeValue.FromList(tagValues); } - messageItem[ExtractedTagsAttribute] = new AttributeValue { M = tagsMap }; + messageItem[ExtractedTagsAttribute] = AttributeValue.FromMap(tagsMap); } // Inherit conversation TTL if (_configuration.DefaultTtl.HasValue) { var expiresAt = now.Add(_configuration.DefaultTtl.Value).ToUnixTimeSeconds(); - messageItem[_configuration.TtlAttributeName] = new AttributeValue { N = expiresAt.ToString() }; + messageItem[_configuration.TtlAttributeName] = AttributeValue.Number(expiresAt.ToString()); } transactItems.Add(new TransactWriteItem @@ -343,8 +343,8 @@ private async Task>> AddMessagesInter var updateExpressionValues = new Dictionary { - [":msgCount"] = new AttributeValue { N = messageList.Count.ToString() }, - [":updatedAt"] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() } + [":msgCount"] = AttributeValue.Number(messageList.Count.ToString()), + [":updatedAt"] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString()) }; if (totalTokens > 0) @@ -352,10 +352,10 @@ private async Task>> AddMessagesInter updateParts.Add($"{TotalInputTokensAttribute} = if_not_exists({TotalInputTokensAttribute}, :zero) + :inputTokens"); updateParts.Add($"{TotalOutputTokensAttribute} = if_not_exists({TotalOutputTokensAttribute}, :zero) + :outputTokens"); updateParts.Add($"{TotalTokensAttribute} = if_not_exists({TotalTokensAttribute}, :zero) + :totalTokens"); - updateExpressionValues[":zero"] = new AttributeValue { N = "0" }; - updateExpressionValues[":inputTokens"] = new AttributeValue { N = totalInputTokens.ToString() }; - updateExpressionValues[":outputTokens"] = new AttributeValue { N = totalOutputTokens.ToString() }; - updateExpressionValues[":totalTokens"] = new AttributeValue { N = totalTokens.ToString() }; + updateExpressionValues[":zero"] = AttributeValue.Number("0"); + updateExpressionValues[":inputTokens"] = AttributeValue.Number(totalInputTokens.ToString()); + updateExpressionValues[":outputTokens"] = AttributeValue.Number(totalOutputTokens.ToString()); + updateExpressionValues[":totalTokens"] = AttributeValue.Number(totalTokens.ToString()); } transactItems.Add(new TransactWriteItem @@ -396,7 +396,7 @@ private async Task>> AddMessagesInter var updateParts = new List { $"{UpdatedAtAttribute} = :updatedAt" }; var expressionAttributeValues = new Dictionary { - [":updatedAt"] = new AttributeValue { N = now.ToUnixTimeSeconds().ToString() } + [":updatedAt"] = AttributeValue.Number(now.ToUnixTimeSeconds().ToString()) }; var expressionAttributeNames = new Dictionary(); @@ -415,7 +415,7 @@ private async Task>> AddMessagesInter if (metadata.Tags.Count > 0) { updateParts.Add($"{TagsAttribute} = :tags"); - expressionAttributeValues[":tags"] = new AttributeValue { SS = metadata.Tags }; + expressionAttributeValues[":tags"] = AttributeValue.FromStringSet(metadata.Tags); } if (metadata.CustomData.Count > 0) @@ -427,7 +427,7 @@ private async Task>> AddMessagesInter { customDataMap[kvp.Key] = kvp.Value; } - expressionAttributeValues[":customData"] = new AttributeValue { M = customDataMap }; + expressionAttributeValues[":customData"] = AttributeValue.FromMap(customDataMap); } var updateRequest = new UpdateItemRequest @@ -488,8 +488,8 @@ public async Task> DeleteConversationAsync(string conversationI { allItems.Add(new Dictionary { - [_configuration.PartitionKeyName] = item[_configuration.PartitionKeyName]!, - [_configuration.SortKeyName] = item[_configuration.SortKeyName]! + [_configuration.PartitionKeyName] = item[_configuration.PartitionKeyName]!.Value, + [_configuration.SortKeyName] = item[_configuration.SortKeyName]!.Value }); } @@ -535,7 +535,7 @@ private static void SerializeMetadata(ConversationMetadata metadata, Dictionary< item[ModelIdAttribute] = metadata.ModelId; if (metadata.Tags.Count > 0) - item[TagsAttribute] = new AttributeValue { SS = metadata.Tags }; + item[TagsAttribute] = AttributeValue.FromStringSet(metadata.Tags); if (metadata.CustomData.Count > 0) { @@ -544,7 +544,7 @@ private static void SerializeMetadata(ConversationMetadata metadata, Dictionary< { customDataMap[kvp.Key] = kvp.Value; } - item[CustomDataAttribute] = new AttributeValue { M = customDataMap }; + item[CustomDataAttribute] = AttributeValue.FromMap(customDataMap); } } diff --git a/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/Internal/ContentBlockSerializer.cs b/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/Internal/ContentBlockSerializer.cs index 825d11c9..95a772d9 100644 --- a/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/Internal/ContentBlockSerializer.cs +++ b/src/Clients/Goa.Clients.Bedrock.Conversation.Dynamo/Internal/ContentBlockSerializer.cs @@ -26,14 +26,11 @@ public static ErrorOr Serialize(ContentBlock block) { if (block.Text is not null) { - return new AttributeValue + return AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - [TypeAttribute] = TextType, - ["text"] = block.Text - } - }; + [TypeAttribute] = TextType, + ["text"] = block.Text + }); } if (block.Image is not null) @@ -55,7 +52,7 @@ public static ErrorOr Serialize(ContentBlock block) imageMap["s3BucketOwner"] = block.Image.Source.S3Location.BucketOwner; } - return new AttributeValue { M = imageMap }; + return AttributeValue.FromMap(imageMap); } if (block.Document is not null) @@ -78,7 +75,7 @@ public static ErrorOr Serialize(ContentBlock block) docMap["s3BucketOwner"] = block.Document.Source.S3Location.BucketOwner; } - return new AttributeValue { M = docMap }; + return AttributeValue.FromMap(docMap); } if (block.ToolUse is not null) @@ -91,7 +88,7 @@ public static ErrorOr Serialize(ContentBlock block) ["input"] = block.ToolUse.Input.GetRawText() }; - return new AttributeValue { M = toolUseMap }; + return AttributeValue.FromMap(toolUseMap); } if (block.ToolResult is not null) @@ -109,13 +106,13 @@ public static ErrorOr Serialize(ContentBlock block) { [TypeAttribute] = ToolResultType, ["toolUseId"] = block.ToolResult.ToolUseId, - ["content"] = new AttributeValue { L = contentList } + ["content"] = AttributeValue.FromList(contentList) }; if (block.ToolResult.Status is not null) toolResultMap["status"] = block.ToolResult.Status; - return new AttributeValue { M = toolResultMap }; + return AttributeValue.FromMap(toolResultMap); } return Error.Validation(ConversationErrorCodes.ContentBlockEmpty, "ContentBlock must have at least one content type set"); diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/MapperGenerator.cs b/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/MapperGenerator.cs index 8eab99ed..3a0487b3 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/MapperGenerator.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/MapperGenerator.cs @@ -173,7 +173,7 @@ private void GenerateConcreteToDynamoRecord(CodeBuilder builder, DynamoTypeInfo var typeNameField = (dynamoModelAttr?.TypeName != "Type" ? dynamoModelAttr?.TypeName : null) ?? GetInheritedDynamoModelAttribute(type)?.TypeName ?? "Type"; - builder.AppendLine($"record[\"{typeNameField}\"] = new AttributeValue {{ S = \"{type.FullName}\" }};"); + builder.AppendLine($"record[\"{typeNameField}\"] = AttributeValue.String(\"{type.FullName}\");"); } // Add property mappings, excluding properties already emitted as GSI keys @@ -187,8 +187,8 @@ private void GeneratePrimaryKeyAssignment(CodeBuilder builder, DynamoTypeInfo ty var pkCode = GenerateKeyCode(type, dynamoAttr.PK, "PK"); var skCode = GenerateKeyCode(type, dynamoAttr.SK, "SK"); - builder.AppendLine($"record[\"{dynamoAttr.PKName}\"] = new AttributeValue {{ S = {pkCode} }};"); - builder.AppendLine($"record[\"{dynamoAttr.SKName}\"] = new AttributeValue {{ S = {skCode} }};"); + builder.AppendLine($"record[\"{dynamoAttr.PKName}\"] = AttributeValue.String({pkCode});"); + builder.AppendLine($"record[\"{dynamoAttr.SKName}\"] = AttributeValue.String({skCode});"); } private void GenerateGSIKeyAssignment(CodeBuilder builder, DynamoTypeInfo type, GSIAttributeInfo gsiAttr) @@ -208,7 +208,7 @@ private void GenerateGSIKeyAssignment(CodeBuilder builder, DynamoTypeInfo type, else { var pkCode = GenerateKeyCode(type, gsiAttr.PK, gsiAttr.PKName!); - builder.AppendLine($"record[\"{gsiAttr.PKName}\"] = new AttributeValue {{ S = {pkCode} }};"); + builder.AppendLine($"record[\"{gsiAttr.PKName}\"] = AttributeValue.String({pkCode});"); } // Generate SK assignment - conditional if single nullable property, otherwise direct @@ -219,7 +219,7 @@ private void GenerateGSIKeyAssignment(CodeBuilder builder, DynamoTypeInfo type, else { var skCode = GenerateKeyCode(type, gsiAttr.SK, gsiAttr.SKName!); - builder.AppendLine($"record[\"{gsiAttr.SKName}\"] = new AttributeValue {{ S = {skCode} }};"); + builder.AppendLine($"record[\"{gsiAttr.SKName}\"] = AttributeValue.String({skCode});"); } } @@ -773,7 +773,7 @@ private void GenerateConditionalGSIKeyAssignment(CodeBuilder builder, string att } builder.OpenBraceWithLine($"if ({nullCheck})"); - builder.AppendLine($"record[\"{attributeName}\"] = new AttributeValue {{ S = {valueCode} }};"); + builder.AppendLine($"record[\"{attributeName}\"] = AttributeValue.String({valueCode});"); builder.CloseBrace(); } diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/CollectionTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/CollectionTypeHandler.cs index fa34b442..c6fdc86d 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/CollectionTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/CollectionTypeHandler.cs @@ -31,7 +31,7 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) if (propertyInfo.ElementType == null || _registry == null) { - return "new AttributeValue { NULL = true }"; // Skip invalid collections + return "AttributeValue.Null()"; // Skip invalid collections } var elementType = propertyInfo.ElementType; @@ -48,11 +48,11 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) var elementMapping = GenerateElementToAttributeValue(elementType, "item"); if (elementMapping != null) { - return $"model.{propertyName} != null ? new AttributeValue {{ L = model.{propertyName}.Select(item => {elementMapping}).ToList() }} : new AttributeValue {{ NULL = true }}"; + return $"model.{propertyName} != null ? AttributeValue.FromList(model.{propertyName}.Select(item => {elementMapping}).ToList()) : AttributeValue.Null()"; } // Fallback to NULL for unsupported types - return "new AttributeValue { NULL = true }"; + return "AttributeValue.Null()"; } /// @@ -68,7 +68,7 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) { // Use just the type name (not the full namespace) to match the mapper naming convention var normalizedTypeName = NamingHelpers.NormalizeTypeName(elementType.Name); - return $"({itemVarName} != null ? new AttributeValue {{ M = DynamoMapper.{normalizedTypeName}.ToDynamoRecord({itemVarName}) }} : new AttributeValue {{ NULL = true }})"; + return $"({itemVarName} != null ? AttributeValue.FromMap(DynamoMapper.{normalizedTypeName}.ToDynamoRecord({itemVarName})) : AttributeValue.Null())"; } // Check if it's a nested collection @@ -89,7 +89,7 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) var nestedElementMapping = GenerateElementToAttributeValue(nestedElementType, nestedVarName); if (nestedElementMapping != null) { - return $"({itemVarName} != null ? new AttributeValue {{ L = {itemVarName}.Select({nestedVarName} => {nestedElementMapping}).ToList() }} : new AttributeValue {{ NULL = true }})"; + return $"({itemVarName} != null ? AttributeValue.FromList({itemVarName}.Select({nestedVarName} => {nestedElementMapping}).ToList()) : AttributeValue.Null())"; } } } @@ -104,16 +104,16 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) { return elementType.SpecialType switch { - SpecialType.System_String => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ SS = {collectionVarName}.ToList() }} : new AttributeValue {{ NULL = true }})", + SpecialType.System_String => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromStringSet({collectionVarName}.ToList()) : AttributeValue.Null())", 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 => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ NS = {collectionVarName}.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList() }} : new AttributeValue {{ NULL = true }})", - SpecialType.System_Boolean => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ SS = {collectionVarName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", - SpecialType.System_DateTime => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ SS = {collectionVarName}.Select(x => x.ToString(\"o\")).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.Name == nameof(Guid) => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ SS = {collectionVarName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.Name == nameof(TimeSpan) => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ SS = {collectionVarName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.Name == nameof(DateTimeOffset) => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ SS = {collectionVarName}.Select(x => x.ToString(\"o\")).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.TypeKind == TypeKind.Enum => $"({collectionVarName} != null && {collectionVarName}.Any() ? new AttributeValue {{ SS = {collectionVarName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", + SpecialType.System_Decimal or SpecialType.System_Single or SpecialType.System_Double => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromNumberSet({collectionVarName}.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList()) : AttributeValue.Null())", + SpecialType.System_Boolean => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromStringSet({collectionVarName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", + SpecialType.System_DateTime => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromStringSet({collectionVarName}.Select(x => x.ToString(\"o\")).ToList()) : AttributeValue.Null())", + _ when elementType.Name == nameof(Guid) => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromStringSet({collectionVarName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", + _ when elementType.Name == nameof(TimeSpan) => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromStringSet({collectionVarName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", + _ when elementType.Name == nameof(DateTimeOffset) => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromStringSet({collectionVarName}.Select(x => x.ToString(\"o\")).ToList()) : AttributeValue.Null())", + _ when elementType.TypeKind == TypeKind.Enum => $"({collectionVarName} != null && {collectionVarName}.Any() ? AttributeValue.FromStringSet({collectionVarName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", _ => null }; } @@ -173,16 +173,16 @@ private static bool IsCollectionType(ITypeSymbol type) { return elementType.SpecialType switch { - SpecialType.System_String => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ SS = model.{propertyName}.ToList() }} : new AttributeValue {{ NULL = true }})", + SpecialType.System_String => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromStringSet(model.{propertyName}.ToList()) : AttributeValue.Null())", 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 => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ NS = model.{propertyName}.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList() }} : new AttributeValue {{ NULL = true }})", - SpecialType.System_Boolean => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ SS = model.{propertyName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", - SpecialType.System_DateTime => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ SS = model.{propertyName}.Select(x => x.ToString(\"o\")).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.Name == nameof(Guid) => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ SS = model.{propertyName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.Name == nameof(TimeSpan) => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ SS = model.{propertyName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.Name == nameof(DateTimeOffset) => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ SS = model.{propertyName}.Select(x => x.ToString(\"o\")).ToList() }} : new AttributeValue {{ NULL = true }})", - _ when elementType.TypeKind == TypeKind.Enum => $"(model.{propertyName} != null && model.{propertyName}.Any() ? new AttributeValue {{ SS = model.{propertyName}.Select(x => x.ToString()).ToList() }} : new AttributeValue {{ NULL = true }})", + SpecialType.System_Decimal or SpecialType.System_Single or SpecialType.System_Double => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromNumberSet(model.{propertyName}.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList()) : AttributeValue.Null())", + SpecialType.System_Boolean => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromStringSet(model.{propertyName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", + SpecialType.System_DateTime => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromStringSet(model.{propertyName}.Select(x => x.ToString(\"o\")).ToList()) : AttributeValue.Null())", + _ when elementType.Name == nameof(Guid) => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromStringSet(model.{propertyName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", + _ when elementType.Name == nameof(TimeSpan) => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromStringSet(model.{propertyName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", + _ when elementType.Name == nameof(DateTimeOffset) => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromStringSet(model.{propertyName}.Select(x => x.ToString(\"o\")).ToList()) : AttributeValue.Null())", + _ when elementType.TypeKind == TypeKind.Enum => $"(model.{propertyName} != null && model.{propertyName}.Any() ? AttributeValue.FromStringSet(model.{propertyName}.Select(x => x.ToString()).ToList()) : AttributeValue.Null())", _ => null // Use composition for complex types }; } @@ -213,7 +213,18 @@ public string GenerateFromDynamoRecord(PropertyInfo propertyInfo, string recordV { var normalizedTypeName = elementType.Name.Replace(".", "_").Replace("`", "_"); var varName = memberName.ToLowerInvariant(); - var sourceExpression = $"{recordVariableName}.TryGetList(\"{memberName}\", out var {varName}List) && {varName}List != null ? {varName}List.Where(item => item.M != null).Select(item => DynamoMapper.{normalizedTypeName}.FromDynamoRecord(new DynamoRecord(item.M!), {pkVariable}, {skVariable})) : null"; + // When element type is explicitly nullable (e.g., List), preserve NULL entries as null + // instead of filtering them out. Non-nullable element types keep the filter to avoid type mismatches. + var isElementNullable = elementType.NullableAnnotation == NullableAnnotation.Annotated; + string sourceExpression; + if (isElementNullable) + { + sourceExpression = $"{recordVariableName}.TryGetList(\"{memberName}\", out var {varName}List) && {varName}List != null ? {varName}List.Select(item => item.M != null ? DynamoMapper.{normalizedTypeName}.FromDynamoRecord(new DynamoRecord(item.M!), {pkVariable}, {skVariable}) : null) : null"; + } + else + { + sourceExpression = $"{recordVariableName}.TryGetList(\"{memberName}\", out var {varName}List) && {varName}List != null ? {varName}List.Where(item => item.M != null).Select(item => DynamoMapper.{normalizedTypeName}.FromDynamoRecord(new DynamoRecord(item.M!), {pkVariable}, {skVariable})) : null"; + } return ConvertToTargetCollectionType(propertyInfo.Type, elementType, sourceExpression); } diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/ComplexTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/ComplexTypeHandler.cs index 4ce8033b..31a517c1 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/ComplexTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/ComplexTypeHandler.cs @@ -68,7 +68,7 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) namedValueType.TypeArguments.Length == 1 && namedValueType.TypeArguments[0].SpecialType == SpecialType.System_String) { - return $"model.{propertyName} != null ? new AttributeValue {{ M = model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null || kvp.Value.Count == 0 ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ SS = kvp.Value }}) }} : new AttributeValue {{ NULL = true }}"; + return $"model.{propertyName} != null ? AttributeValue.FromMap(model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null || kvp.Value.Count == 0 ? AttributeValue.Null() : AttributeValue.FromStringSet(kvp.Value))) : AttributeValue.Null()"; } // Special case: Dictionary> @@ -78,7 +78,7 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) namedValueType2.TypeArguments[0].SpecialType == SpecialType.System_String && namedValueType2.TypeArguments[1].SpecialType == SpecialType.System_String) { - return $"model.{propertyName} != null ? new AttributeValue {{ M = model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue {{ M = (kvp.Value ?? new Dictionary()).ToDictionary(innerKvp => innerKvp.Key, innerKvp => innerKvp.Value != null ? new AttributeValue {{ S = innerKvp.Value }} : new AttributeValue {{ NULL = true }}) }}) }} : new AttributeValue {{ NULL = true }}"; + return $"model.{propertyName} != null ? AttributeValue.FromMap(model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => AttributeValue.FromMap((kvp.Value ?? new Dictionary()).ToDictionary(innerKvp => innerKvp.Key, innerKvp => innerKvp.Value != null ? AttributeValue.String(innerKvp.Value) : AttributeValue.Null())))) : AttributeValue.Null()"; } // Fallback to primitive handling for other types @@ -86,17 +86,34 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) } // For unsupported dictionary types, return NULL - return "new AttributeValue { NULL = true }"; + return "AttributeValue.Null()"; } // Handle complex types (nested objects) var normalizedTypeName = propertyInfo.UnderlyingType.Name.Replace(".", "_").Replace("`", "_"); - return $"model.{propertyName} != null ? new AttributeValue {{ M = DynamoMapper.{normalizedTypeName}.ToDynamoRecord(model.{propertyName}) }} : new AttributeValue {{ NULL = true }}"; + var isNullable = propertyInfo.IsNullable; + var isValueType = propertyInfo.Type.IsValueType; + + if (isValueType && !isNullable) + { + // Non-nullable struct: always has a value, no null check needed + return $"AttributeValue.FromMap(DynamoMapper.{normalizedTypeName}.ToDynamoRecord(model.{propertyName}))"; + } + else if (isValueType && isNullable) + { + // Nullable struct: check for null, unwrap with .Value + return $"model.{propertyName} != null ? AttributeValue.FromMap(DynamoMapper.{normalizedTypeName}.ToDynamoRecord(model.{propertyName}.Value)) : AttributeValue.Null()"; + } + else + { + // Reference type: standard null check + return $"model.{propertyName} != null ? AttributeValue.FromMap(DynamoMapper.{normalizedTypeName}.ToDynamoRecord(model.{propertyName})) : AttributeValue.Null()"; + } } public string GenerateFromDynamoRecord(PropertyInfo propertyInfo, string recordVariableName, string pkVariable, string skVariable) { - var memberName = propertyInfo.Name; + var memberName = propertyInfo.GetDynamoAttributeName(); var typeName = propertyInfo.Type.ToDisplayString(); // Check for dictionary types @@ -326,22 +343,22 @@ private string GeneratePrimitiveDictionary(string propertyName, ITypeSymbol valu { if (valueType.SpecialType == SpecialType.System_String) { - return $"model.{propertyName} != null ? new AttributeValue {{ M = model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue {{ S = kvp.Value }}) }} : new AttributeValue {{ NULL = true }}"; + return $"model.{propertyName} != null ? AttributeValue.FromMap(model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => kvp.Value != null ? AttributeValue.String(kvp.Value) : AttributeValue.Null())) : AttributeValue.Null()"; } else if (IsNumericType(valueType)) { - return $"model.{propertyName} != null ? new AttributeValue {{ M = model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue {{ N = kvp.Value.ToString(CultureInfo.InvariantCulture) }}) }} : new AttributeValue {{ NULL = true }}"; + return $"model.{propertyName} != null ? AttributeValue.FromMap(model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => AttributeValue.Number(kvp.Value.ToString(CultureInfo.InvariantCulture)))) : AttributeValue.Null()"; } else if (valueType.SpecialType == SpecialType.System_DateTime) { - return $"model.{propertyName} != null ? new AttributeValue {{ M = model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue {{ S = kvp.Value.ToString(\"o\") }}) }} : new AttributeValue {{ NULL = true }}"; + return $"model.{propertyName} != null ? AttributeValue.FromMap(model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => AttributeValue.String(kvp.Value.ToString(\"o\")))) : AttributeValue.Null()"; } else if (valueType.TypeKind == TypeKind.Enum) { - return $"model.{propertyName} != null ? new AttributeValue {{ M = model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue {{ S = kvp.Value.ToString() }}) }} : new AttributeValue {{ NULL = true }}"; + return $"model.{propertyName} != null ? AttributeValue.FromMap(model.{propertyName}.ToDictionary(kvp => kvp.Key, kvp => AttributeValue.String(kvp.Value.ToString()))) : AttributeValue.Null()"; } - + // For unsupported types, return NULL - return "new AttributeValue { NULL = true }"; + return "AttributeValue.Null()"; } } \ No newline at end of file diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateOnlyTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateOnlyTypeHandler.cs index 9a08de27..fe4c8c14 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateOnlyTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateOnlyTypeHandler.cs @@ -24,12 +24,12 @@ public bool CanHandle(PropertyInfo propertyInfo) #pragma warning disable CS8603 // Possible null reference return - intentional for conditional assignment ? null // Use conditional assignment instead #pragma warning restore CS8603 - : $"new AttributeValue {{ S = model.{propertyName}.ToString(\"yyyy-MM-dd\") }}"; + : $"AttributeValue.String(model.{propertyName}.ToString(\"yyyy-MM-dd\"))"; } public string GenerateFromDynamoRecord(PropertyInfo propertyInfo, string recordVariableName, string pkVariable, string skVariable) { - var memberName = propertyInfo.Name; + var memberName = propertyInfo.GetDynamoAttributeName(); var isNullable = propertyInfo.IsNullable; // Avoid variable name conflicts with pk/sk extraction variables @@ -52,16 +52,17 @@ public string GenerateKeyFormatting(PropertyInfo propertyInfo) public string? GenerateConditionalAssignment(PropertyInfo propertyInfo, string recordVariable) { var propertyName = propertyInfo.Name; + var dynamoAttributeName = propertyInfo.GetDynamoAttributeName(); var isNullable = propertyInfo.IsNullable; - + if (!isNullable) { return null; } - + return $@"if (model.{propertyName}.HasValue) {{ - {recordVariable}[""{propertyName}""] = new AttributeValue {{ S = model.{propertyName}.Value.ToString(""yyyy-MM-dd"") }}; + {recordVariable}[""{dynamoAttributeName}""] = AttributeValue.String(model.{propertyName}.Value.ToString(""yyyy-MM-dd"")); }}"; } diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateTimeTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateTimeTypeHandler.cs index b8dc1e64..733137d6 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateTimeTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/DateTimeTypeHandler.cs @@ -33,22 +33,23 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) #pragma warning disable CS8603 // Possible null reference return - intentional for conditional assignment return isNullable ? null // This will trigger conditional assignment generation in MapperGenerator - : $"new AttributeValue {{ S = model.{propertyName}.ToString(\"o\") }}"; + : $"AttributeValue.String(model.{propertyName}.ToString(\"o\"))"; #pragma warning restore CS8603 } public string GenerateConditionalAssignment(PropertyInfo propertyInfo, string recordVariable) { var propertyName = propertyInfo.Name; + var dynamoAttributeName = propertyInfo.GetDynamoAttributeName(); return $@"if (model.{propertyName}.HasValue) {{ - {recordVariable}[""{propertyName}""] = new AttributeValue {{ S = model.{propertyName}.Value.ToString(""o"") }}; + {recordVariable}[""{dynamoAttributeName}""] = AttributeValue.String(model.{propertyName}.Value.ToString(""o"")); }}"; } public string GenerateFromDynamoRecord(PropertyInfo propertyInfo, string recordVariableName, string pkVariable, string skVariable) { - var memberName = propertyInfo.Name; + var memberName = propertyInfo.GetDynamoAttributeName(); var underlyingType = propertyInfo.UnderlyingType; var isNullable = propertyInfo.IsNullable; diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/EnumTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/EnumTypeHandler.cs index 19e472be..8c9ce124 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/EnumTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/EnumTypeHandler.cs @@ -25,12 +25,12 @@ public bool CanHandle(PropertyInfo propertyInfo) #pragma warning disable CS8603 // Possible null reference return - intentional for conditional assignment ? null // Use conditional assignment instead #pragma warning restore CS8603 - : $"new AttributeValue {{ S = model.{propertyName}.ToString() }}"; + : $"AttributeValue.String(model.{propertyName}.ToString())"; } public string GenerateFromDynamoRecord(PropertyInfo propertyInfo, string recordVariableName, string pkVariable, string skVariable) { - var memberName = propertyInfo.Name; + var memberName = propertyInfo.GetDynamoAttributeName(); var enumType = propertyInfo.UnderlyingType.ToDisplayString(); var isNullable = propertyInfo.IsNullable; @@ -59,16 +59,17 @@ public string GenerateKeyFormatting(PropertyInfo propertyInfo) public string? GenerateConditionalAssignment(PropertyInfo propertyInfo, string recordVariable) { var propertyName = propertyInfo.Name; + var dynamoAttributeName = propertyInfo.GetDynamoAttributeName(); var isNullable = propertyInfo.IsNullable; - + if (!isNullable) { return null; } - + return $@"if (model.{propertyName}.HasValue) {{ - {recordVariable}[""{propertyName}""] = new AttributeValue {{ S = model.{propertyName}.Value.ToString() }}; + {recordVariable}[""{dynamoAttributeName}""] = AttributeValue.String(model.{propertyName}.Value.ToString()); }}"; } diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/PrimitiveTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/PrimitiveTypeHandler.cs index 45981535..34fd3f86 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/PrimitiveTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/PrimitiveTypeHandler.cs @@ -52,35 +52,35 @@ 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 => - $"new AttributeValue {{ N = model.{propertyName}.ToString(CultureInfo.InvariantCulture) }}", - SpecialType.System_Boolean when isNullable => + $"AttributeValue.Number(model.{propertyName}.ToString(CultureInfo.InvariantCulture))", + SpecialType.System_Boolean when isNullable => null, // Use conditional assignment instead - SpecialType.System_Boolean => - $"new AttributeValue {{ BOOL = model.{propertyName} }}", - SpecialType.System_Char when isNullable => + SpecialType.System_Boolean => + $"AttributeValue.Bool(model.{propertyName})", + SpecialType.System_Char when isNullable => null, // Use conditional assignment instead - SpecialType.System_Char => - $"new AttributeValue {{ S = model.{propertyName}.ToString() }}", - SpecialType.System_DateTime when isNullable => + SpecialType.System_Char => + $"AttributeValue.String(model.{propertyName}.ToString())", + SpecialType.System_DateTime when isNullable => null, // Use conditional assignment instead (handled by DateTimeTypeHandler) - SpecialType.System_DateTime => - $"new AttributeValue {{ S = model.{propertyName}.ToString(\\\"o\\\") }}", - _ when underlyingType.Name == nameof(Guid) && isNullable => + SpecialType.System_DateTime => + $"AttributeValue.String(model.{propertyName}.ToString(\"o\"))", + _ when underlyingType.Name == nameof(Guid) && isNullable => null, // Use conditional assignment instead - _ when underlyingType.Name == nameof(Guid) => - $"new AttributeValue {{ S = model.{propertyName}.ToString() }}", - _ when underlyingType.Name == nameof(TimeSpan) && isNullable => + _ when underlyingType.Name == nameof(Guid) => + $"AttributeValue.String(model.{propertyName}.ToString())", + _ when underlyingType.Name == nameof(TimeSpan) && isNullable => null, // Use conditional assignment instead - _ when underlyingType.Name == nameof(TimeSpan) => - $"new AttributeValue {{ S = model.{propertyName}.ToString() }}", - _ when underlyingType.Name == nameof(DateTimeOffset) && isNullable => + _ when underlyingType.Name == nameof(TimeSpan) => + $"AttributeValue.String(model.{propertyName}.ToString())", + _ when underlyingType.Name == nameof(DateTimeOffset) && isNullable => null, // Use conditional assignment instead - _ when underlyingType.Name == nameof(DateTimeOffset) => - $"new AttributeValue {{ S = model.{propertyName}.ToString(\\\"o\\\") }}", - _ when underlyingType.TypeKind == TypeKind.Enum && isNullable => + _ when underlyingType.Name == nameof(DateTimeOffset) => + $"AttributeValue.String(model.{propertyName}.ToString(\"o\"))", + _ when underlyingType.TypeKind == TypeKind.Enum && isNullable => null, // Use conditional assignment instead - _ when underlyingType.TypeKind == TypeKind.Enum => - $"new AttributeValue {{ S = model.{propertyName}.ToString() }}", + _ when underlyingType.TypeKind == TypeKind.Enum => + $"AttributeValue.String(model.{propertyName}.ToString())", _ => string.Empty }; } @@ -161,27 +161,27 @@ public string GenerateKeyFormatting(PropertyInfo propertyInfo) var attributeValue = underlyingType.SpecialType switch { SpecialType.System_String => - $"new AttributeValue {{ S = model.{propertyName} }}", + $"AttributeValue.String(model.{propertyName})", 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 => - $"new AttributeValue {{ N = model.{propertyName}.Value.ToString(CultureInfo.InvariantCulture) }}", + $"AttributeValue.Number(model.{propertyName}.Value.ToString(CultureInfo.InvariantCulture))", SpecialType.System_Boolean => - $"new AttributeValue {{ BOOL = model.{propertyName}.Value }}", + $"AttributeValue.Bool(model.{propertyName}.Value)", SpecialType.System_Char => - $"new AttributeValue {{ S = model.{propertyName}.Value.ToString() }}", + $"AttributeValue.String(model.{propertyName}.Value.ToString())", SpecialType.System_DateTime => - $"new AttributeValue {{ S = model.{propertyName}.Value.ToString(\\\"o\\\") }}", + $"AttributeValue.String(model.{propertyName}.Value.ToString(\"o\"))", _ when underlyingType.Name == nameof(Guid) => - $"new AttributeValue {{ S = model.{propertyName}.Value.ToString() }}", + $"AttributeValue.String(model.{propertyName}.Value.ToString())", _ when underlyingType.Name == nameof(TimeSpan) => - $"new AttributeValue {{ S = model.{propertyName}.Value.ToString() }}", + $"AttributeValue.String(model.{propertyName}.Value.ToString())", _ when underlyingType.Name == nameof(DateTimeOffset) => - $"new AttributeValue {{ S = model.{propertyName}.Value.ToString(\\\"o\\\") }}", + $"AttributeValue.String(model.{propertyName}.Value.ToString(\"o\"))", _ when underlyingType.TypeKind == TypeKind.Enum => - $"new AttributeValue {{ S = model.{propertyName}.Value.ToString() }}", + $"AttributeValue.String(model.{propertyName}.Value.ToString())", _ => null }; diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TimeOnlyTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TimeOnlyTypeHandler.cs index 6f12c44c..26de8d6b 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TimeOnlyTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TimeOnlyTypeHandler.cs @@ -24,12 +24,12 @@ public bool CanHandle(PropertyInfo propertyInfo) #pragma warning disable CS8603 // Possible null reference return - intentional for conditional assignment ? null // Use conditional assignment instead #pragma warning restore CS8603 - : $"new AttributeValue {{ S = model.{propertyName}.ToString(\"HH:mm:ss.fffffff\") }}"; + : $"AttributeValue.String(model.{propertyName}.ToString(\"HH:mm:ss.fffffff\"))"; } public string GenerateFromDynamoRecord(PropertyInfo propertyInfo, string recordVariableName, string pkVariable, string skVariable) { - var memberName = propertyInfo.Name; + var memberName = propertyInfo.GetDynamoAttributeName(); var isNullable = propertyInfo.IsNullable; // Avoid variable name conflicts with pk/sk extraction variables @@ -52,16 +52,17 @@ public string GenerateKeyFormatting(PropertyInfo propertyInfo) public string? GenerateConditionalAssignment(PropertyInfo propertyInfo, string recordVariable) { var propertyName = propertyInfo.Name; + var dynamoAttributeName = propertyInfo.GetDynamoAttributeName(); var isNullable = propertyInfo.IsNullable; - + if (!isNullable) { return null; } - + return $@"if (model.{propertyName}.HasValue) {{ - {recordVariable}[""{propertyName}""] = new AttributeValue {{ S = model.{propertyName}.Value.ToString(""HH:mm:ss.fffffff"") }}; + {recordVariable}[""{dynamoAttributeName}""] = AttributeValue.String(model.{propertyName}.Value.ToString(""HH:mm:ss.fffffff"")); }}"; } diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TypeHandlerRegistry.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TypeHandlerRegistry.cs index 3519a9cf..2b72c4d0 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TypeHandlerRegistry.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/TypeHandlerRegistry.cs @@ -105,7 +105,7 @@ public string GenerateNestedToAttributeValue(ITypeSymbol type, string valueExpre { if (depth > MaxRecursionDepth) { - return "new AttributeValue { NULL = true }"; // Prevent infinite recursion + return "AttributeValue.Null()"; // Prevent infinite recursion } // Create a temporary PropertyInfo for the nested type @@ -114,12 +114,12 @@ public string GenerateNestedToAttributeValue(ITypeSymbol type, string valueExpre if (handler == null) { - return "new AttributeValue { NULL = true }"; + return "AttributeValue.Null()"; } // Replace the property access with the provided value expression var attributeValueCode = handler.GenerateToAttributeValue(nestedProperty); - return attributeValueCode?.Replace($"model.{nestedProperty.Name}", valueExpression) ?? "new AttributeValue { NULL = true }"; + return attributeValueCode?.Replace($"model.{nestedProperty.Name}", valueExpression) ?? "AttributeValue.Null()"; } /// diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnixTimestampTypeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnixTimestampTypeHandler.cs index 4eeea4dd..b429d800 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnixTimestampTypeHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnixTimestampTypeHandler.cs @@ -35,8 +35,8 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) else { return isMilliseconds - ? $"new AttributeValue {{ N = ((DateTimeOffset)model.{propertyName}).ToUnixTimeMilliseconds().ToString() }}" - : $"new AttributeValue {{ N = ((DateTimeOffset)model.{propertyName}).ToUnixTimeSeconds().ToString() }}"; + ? $"AttributeValue.Number(((DateTimeOffset)model.{propertyName}).ToUnixTimeMilliseconds().ToString())" + : $"AttributeValue.Number(((DateTimeOffset)model.{propertyName}).ToUnixTimeSeconds().ToString())"; } } @@ -57,11 +57,11 @@ public string GenerateToAttributeValue(PropertyInfo propertyInfo) return isMilliseconds ? $@"if (model.{propertyName}.HasValue) {{ - {recordVariable}[""{dynamoAttributeName}""] = new AttributeValue {{ N = ((DateTimeOffset)model.{propertyName}.Value).ToUnixTimeMilliseconds().ToString() }}; + {recordVariable}[""{dynamoAttributeName}""] = AttributeValue.Number(((DateTimeOffset)model.{propertyName}.Value).ToUnixTimeMilliseconds().ToString()); }}" : $@"if (model.{propertyName}.HasValue) {{ - {recordVariable}[""{dynamoAttributeName}""] = new AttributeValue {{ N = ((DateTimeOffset)model.{propertyName}.Value).ToUnixTimeSeconds().ToString() }}; + {recordVariable}[""{dynamoAttributeName}""] = AttributeValue.Number(((DateTimeOffset)model.{propertyName}.Value).ToUnixTimeSeconds().ToString()); }}"; } diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnsupportedDictionaryHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnsupportedDictionaryHandler.cs index 8dd8cbd3..5a2dc57f 100644 --- a/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnsupportedDictionaryHandler.cs +++ b/src/Clients/Goa.Clients.Dynamo.Generator/TypeHandlers/UnsupportedDictionaryHandler.cs @@ -25,7 +25,7 @@ public bool CanHandle(PropertyInfo propertyInfo) public string GenerateToAttributeValue(PropertyInfo propertyInfo) { // Unsupported dictionary types always serialize as NULL - return "new AttributeValue { NULL = true }"; + return "AttributeValue.Null()"; } public string GenerateFromDynamoRecord(PropertyInfo propertyInfo, string recordVariableName, string pkVariable, string skVariable) diff --git a/src/Clients/Goa.Clients.Dynamo/Extensions/DynamoRecordExtensions.cs b/src/Clients/Goa.Clients.Dynamo/Extensions/DynamoRecordExtensions.cs index ea447aab..7202c357 100644 --- a/src/Clients/Goa.Clients.Dynamo/Extensions/DynamoRecordExtensions.cs +++ b/src/Clients/Goa.Clients.Dynamo/Extensions/DynamoRecordExtensions.cs @@ -20,7 +20,7 @@ public static bool TryGetNullableString(this DynamoRecord record, string columnN if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -46,7 +46,7 @@ public static bool TryGetString(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true || attributeValue.S == null) + if (attributeValue.Type == AttributeType.Null || attributeValue.S is null) return false; value = attributeValue.S; @@ -66,7 +66,7 @@ public static bool TryGetBool(this DynamoRecord record, string columnName, out b if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; if (attributeValue.BOOL == null) @@ -89,7 +89,7 @@ public static bool TryGetNullableBool(this DynamoRecord record, string columnNam if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -112,7 +112,7 @@ public static bool TryGetShort(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && short.TryParse(attributeValue.N, out value); @@ -131,7 +131,7 @@ public static bool TryGetNullableShort(this DynamoRecord record, string columnNa if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -161,7 +161,7 @@ public static bool TryGetInt(this DynamoRecord record, string columnName, out in if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && int.TryParse(attributeValue.N, out value); @@ -180,7 +180,7 @@ public static bool TryGetNullableInt(this DynamoRecord record, string columnName if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -210,7 +210,7 @@ public static bool TryGetLong(this DynamoRecord record, string columnName, out l if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && long.TryParse(attributeValue.N, out value); @@ -229,7 +229,7 @@ public static bool TryGetNullableLong(this DynamoRecord record, string columnNam if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -259,7 +259,7 @@ public static bool TryGetDouble(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && double.TryParse(attributeValue.N, out value); @@ -278,7 +278,7 @@ public static bool TryGetNullableDouble(this DynamoRecord record, string columnN if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -308,7 +308,7 @@ public static bool TryGetFloat(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && float.TryParse(attributeValue.N, out value); @@ -327,7 +327,7 @@ public static bool TryGetNullableFloat(this DynamoRecord record, string columnNa if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -357,7 +357,7 @@ public static bool TryGetDecimal(this DynamoRecord record, string columnName, ou if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && decimal.TryParse(attributeValue.N, out value); @@ -376,7 +376,7 @@ public static bool TryGetNullableDecimal(this DynamoRecord record, string column if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -406,7 +406,7 @@ public static bool TryGetDateTime(this DynamoRecord record, string columnName, o if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.S) && DateTime.TryParse(attributeValue.S, out value); @@ -425,7 +425,7 @@ public static bool TryGetNullableDateTime(this DynamoRecord record, string colum if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -456,7 +456,7 @@ public static bool TryGetEnum(this DynamoRecord record, string columnName, ou if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.S) && Enum.TryParse(attributeValue.S, out value); @@ -476,7 +476,7 @@ public static bool TryGetNullableEnum(this DynamoRecord record, string column if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -506,7 +506,7 @@ public static bool TryGetStringSet(this DynamoRecord record, string columnName, if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = Enumerable.Empty(); return true; @@ -532,7 +532,7 @@ public static bool TryGetIntSet(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = Enumerable.Empty(); return true; @@ -560,7 +560,7 @@ public static bool TryGetLongSet(this DynamoRecord record, string columnName, ou if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = Enumerable.Empty(); return true; @@ -588,7 +588,7 @@ public static bool TryGetDoubleSet(this DynamoRecord record, string columnName, if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = Enumerable.Empty(); return true; @@ -617,7 +617,7 @@ public static bool TryGetEnumSet(this DynamoRecord record, string columnName, if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = Enumerable.Empty(); return true; @@ -645,7 +645,7 @@ public static bool TryGetByte(this DynamoRecord record, string columnName, out b if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && byte.TryParse(attributeValue.N, out value); @@ -664,7 +664,7 @@ public static bool TryGetNullableByte(this DynamoRecord record, string columnNam if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -694,7 +694,7 @@ public static bool TryGetSByte(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && sbyte.TryParse(attributeValue.N, out value); @@ -713,7 +713,7 @@ public static bool TryGetNullableSByte(this DynamoRecord record, string columnNa if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -743,7 +743,7 @@ public static bool TryGetUShort(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && ushort.TryParse(attributeValue.N, out value); @@ -762,7 +762,7 @@ public static bool TryGetNullableUShort(this DynamoRecord record, string columnN if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -792,7 +792,7 @@ public static bool TryGetUInt(this DynamoRecord record, string columnName, out u if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && uint.TryParse(attributeValue.N, out value); @@ -811,7 +811,7 @@ public static bool TryGetNullableUInt(this DynamoRecord record, string columnNam if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -841,7 +841,7 @@ public static bool TryGetULong(this DynamoRecord record, string columnName, out if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.N) && ulong.TryParse(attributeValue.N, out value); @@ -860,7 +860,7 @@ public static bool TryGetNullableULong(this DynamoRecord record, string columnNa if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -890,7 +890,7 @@ public static bool TryGetDateTimeOffset(this DynamoRecord record, string columnN if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.S) && DateTimeOffset.TryParse(attributeValue.S, out value); @@ -909,7 +909,7 @@ public static bool TryGetNullableDateTimeOffset(this DynamoRecord record, string if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -939,7 +939,7 @@ public static bool TryGetTimeSpan(this DynamoRecord record, string columnName, o if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.S) && TimeSpan.TryParse(attributeValue.S, out value); @@ -958,7 +958,7 @@ public static bool TryGetNullableTimeSpan(this DynamoRecord record, string colum if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -988,7 +988,7 @@ public static bool TryGetGuid(this DynamoRecord record, string columnName, out G if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; return !string.IsNullOrEmpty(attributeValue.S) && Guid.TryParse(attributeValue.S, out value); @@ -1007,7 +1007,7 @@ public static bool TryGetNullableGuid(this DynamoRecord record, string columnNam if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1037,7 +1037,7 @@ public static bool TryGetDateTimeSet(this DynamoRecord record, string columnName if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = Enumerable.Empty(); return true; @@ -1065,7 +1065,7 @@ public static bool TryGetStringDictionary(this DynamoRecord record, string colum if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return true; // Empty dictionary for NULL if (attributeValue.M == null) @@ -1073,7 +1073,7 @@ public static bool TryGetStringDictionary(this DynamoRecord record, string colum foreach (var kvp in attributeValue.M) { - if (kvp.Value?.S != null) + if (kvp.Value.S != null) { value[kvp.Key] = kvp.Value.S; } @@ -1094,7 +1094,7 @@ public static bool TryGetNullableStringDictionary(this DynamoRecord record, stri if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1106,7 +1106,7 @@ public static bool TryGetNullableStringDictionary(this DynamoRecord record, stri value = new Dictionary(); foreach (var kvp in attributeValue.M) { - if (kvp.Value?.S != null) + if (kvp.Value.S != null) { value[kvp.Key] = kvp.Value.S; } @@ -1127,7 +1127,7 @@ public static bool TryGetStringIntDictionary(this DynamoRecord record, string co if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return true; // Empty dictionary for NULL if (attributeValue.M == null) @@ -1135,7 +1135,7 @@ public static bool TryGetStringIntDictionary(this DynamoRecord record, string co foreach (var kvp in attributeValue.M) { - if (kvp.Value?.N != null && int.TryParse(kvp.Value.N, out var intValue)) + if (kvp.Value.N != null && int.TryParse(kvp.Value.N, out var intValue)) { value[kvp.Key] = intValue; } @@ -1156,7 +1156,7 @@ public static bool TryGetNullableStringIntDictionary(this DynamoRecord record, s if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1168,7 +1168,7 @@ public static bool TryGetNullableStringIntDictionary(this DynamoRecord record, s value = new Dictionary(); foreach (var kvp in attributeValue.M) { - if (kvp.Value?.N != null && int.TryParse(kvp.Value.N, out var intValue)) + if (kvp.Value.N != null && int.TryParse(kvp.Value.N, out var intValue)) { value[kvp.Key] = intValue; } @@ -1189,7 +1189,7 @@ public static bool TryGetUnixTimestampSeconds(this DynamoRecord record, string c if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; if (string.IsNullOrEmpty(attributeValue.N)) @@ -1223,7 +1223,7 @@ public static bool TryGetNullableUnixTimestampSeconds(this DynamoRecord record, if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1260,7 +1260,7 @@ public static bool TryGetUnixTimestampMilliseconds(this DynamoRecord record, str if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; if (string.IsNullOrEmpty(attributeValue.N)) @@ -1294,7 +1294,7 @@ public static bool TryGetNullableUnixTimestampMilliseconds(this DynamoRecord rec if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1331,7 +1331,7 @@ public static bool TryGetUnixTimestampSecondsAsOffset(this DynamoRecord record, if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; if (string.IsNullOrEmpty(attributeValue.N)) @@ -1365,7 +1365,7 @@ public static bool TryGetNullableUnixTimestampSecondsAsOffset(this DynamoRecord if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1402,7 +1402,7 @@ public static bool TryGetUnixTimestampMillisecondsAsOffset(this DynamoRecord rec if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) return false; if (string.IsNullOrEmpty(attributeValue.N)) @@ -1436,7 +1436,7 @@ public static bool TryGetNullableUnixTimestampMillisecondsAsOffset(this DynamoRe if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1473,7 +1473,7 @@ public static bool TryGetMap(this DynamoRecord record, string columnName, out Dy if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; @@ -1499,7 +1499,7 @@ public static bool TryGetList(this DynamoRecord record, string columnName, out L if (!record.TryGetValue(columnName, out var attributeValue)) return false; - if (attributeValue == null || attributeValue.NULL == true) + if (attributeValue.Type == AttributeType.Null) { value = null; return true; diff --git a/src/Clients/Goa.Clients.Dynamo/Internal/DynamoResponseReader.cs b/src/Clients/Goa.Clients.Dynamo/Internal/DynamoResponseReader.cs index 940da064..f5ab7b83 100644 --- a/src/Clients/Goa.Clients.Dynamo/Internal/DynamoResponseReader.cs +++ b/src/Clients/Goa.Clients.Dynamo/Internal/DynamoResponseReader.cs @@ -190,67 +190,94 @@ private static Dictionary ReadAttributeMap(ref Utf8JsonR private static AttributeValue ReadAttributeValue(ref Utf8JsonReader reader) { - var attr = new AttributeValue(); - // reader is at StartObject + // reader is at StartObject — DynamoDB type wrapper has a single type-value pair while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { if (reader.ValueTextEquals("S"u8)) { reader.Read(); - attr.S = reader.GetString(); - } - else if (reader.ValueTextEquals("N"u8)) - { - reader.Read(); - attr.N = reader.GetString(); + var v = reader.GetString()!; + ReadEndObject(ref reader); + return AttributeValue.String(v); } - else if (reader.ValueTextEquals("BOOL"u8)) + if (reader.ValueTextEquals("N"u8)) { reader.Read(); - attr.BOOL = reader.GetBoolean(); + var v = reader.GetString()!; + ReadEndObject(ref reader); + return AttributeValue.Number(v); } - else if (reader.ValueTextEquals("NULL"u8)) + if (reader.ValueTextEquals("BOOL"u8)) { reader.Read(); - attr.NULL = reader.GetBoolean(); + var v = reader.GetBoolean(); + ReadEndObject(ref reader); + return AttributeValue.Bool(v); } - else if (reader.ValueTextEquals("SS"u8)) + if (reader.ValueTextEquals("NULL"u8)) { reader.Read(); - attr.SS = ReadStringArray(ref reader); + var isNull = reader.GetBoolean(); + ReadEndObject(ref reader); + if (!isNull) + throw new JsonException("Invalid NULL attribute value: expected true but got false"); + return AttributeValue.Null(); } - else if (reader.ValueTextEquals("NS"u8)) + if (reader.ValueTextEquals("SS"u8)) { reader.Read(); - attr.NS = ReadStringArray(ref reader); + var v = ReadStringArray(ref reader); + ReadEndObject(ref reader); + return AttributeValue.FromStringSet(v); } - else if (reader.ValueTextEquals("L"u8)) + if (reader.ValueTextEquals("NS"u8)) { reader.Read(); - attr.L = ReadAttributeValueList(ref reader); + var v = ReadStringArray(ref reader); + ReadEndObject(ref reader); + return AttributeValue.FromNumberSet(v); } - else if (reader.ValueTextEquals("M"u8)) + if (reader.ValueTextEquals("L"u8)) { reader.Read(); - attr.M = ReadAttributeMap(ref reader); + var v = ReadAttributeValueList(ref reader); + ReadEndObject(ref reader); + return AttributeValue.FromList(v); } - else if (reader.ValueTextEquals("B"u8)) + if (reader.ValueTextEquals("M"u8)) { reader.Read(); - attr.B = Convert.FromBase64String(reader.GetString()!); + var v = ReadAttributeMap(ref reader); + ReadEndObject(ref reader); + return AttributeValue.FromMap(v); } - else if (reader.ValueTextEquals("BS"u8)) + if (reader.ValueTextEquals("B"u8)) { reader.Read(); - attr.BS = ReadBinaryArray(ref reader); + var v = Convert.FromBase64String(reader.GetString()!); + ReadEndObject(ref reader); + return AttributeValue.FromBinary(v); } - else + if (reader.ValueTextEquals("BS"u8)) { reader.Read(); - reader.Skip(); + var v = ReadBinaryArray(ref reader); + ReadEndObject(ref reader); + return AttributeValue.FromBinarySet(v); } + + // Skip unknown type + reader.Read(); + reader.Skip(); } - return attr; + throw new JsonException("Unrecognized DynamoDB attribute type: no recognized type property found in attribute value object"); + } + + private static void ReadEndObject(ref Utf8JsonReader reader) + { + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + throw new JsonException("Expected end of attribute value wrapper object"); } private static List ReadStringArray(ref Utf8JsonReader reader) diff --git a/src/Clients/Goa.Clients.Dynamo/Models/AttributeType.cs b/src/Clients/Goa.Clients.Dynamo/Models/AttributeType.cs new file mode 100644 index 00000000..3ce7b3c3 --- /dev/null +++ b/src/Clients/Goa.Clients.Dynamo/Models/AttributeType.cs @@ -0,0 +1,28 @@ +namespace Goa.Clients.Dynamo.Models; + +/// +/// Represents the DynamoDB attribute value type. +/// +public enum AttributeType : byte +{ + /// An attribute of type String. + String, + /// An attribute of type Number (stored as string). + Number, + /// An attribute of type Boolean. + Bool, + /// An attribute of type Null. + Null, + /// An attribute of type String Set. + StringSet, + /// An attribute of type Number Set. + NumberSet, + /// An attribute of type List. + List, + /// An attribute of type Map. + Map, + /// An attribute of type Binary. + Binary, + /// An attribute of type Binary Set. + BinarySet +} diff --git a/src/Clients/Goa.Clients.Dynamo/Models/AttributeValue.cs b/src/Clients/Goa.Clients.Dynamo/Models/AttributeValue.cs index 8ed07b5a..734dfa4f 100644 --- a/src/Clients/Goa.Clients.Dynamo/Models/AttributeValue.cs +++ b/src/Clients/Goa.Clients.Dynamo/Models/AttributeValue.cs @@ -1,108 +1,147 @@ using System.Globalization; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Goa.Clients.Dynamo.Models; /// /// Represents a DynamoDB attribute value with type information for proper serialization. -/// This class mirrors AWS DynamoDB AttributeValue structure but without SDK dependencies. +/// Uses a union layout to avoid heap allocations for each attribute value. /// -public class AttributeValue +[StructLayout(LayoutKind.Explicit, Pack = 1)] +[JsonConverter(typeof(AttributeValueJsonConverter))] +public readonly struct AttributeValue { - /// - /// An attribute of type String. Strings are UTF-8 encoded and can be up to 400 KB in size. - /// - public string? S { get; set; } + [FieldOffset(0)] private readonly object? _referenceValue; // string, List<>, Dictionary<> + [FieldOffset(8)] private readonly AttributeType _type; + [FieldOffset(9)] private readonly bool _boolValue; /// - /// An attribute of type Number. Numbers are sent across the network to DynamoDB as strings, - /// to maximize compatibility across platforms and languages. + /// Gets the type of this attribute value. /// - public string? N { get; set; } + public AttributeType Type => _type; - /// - /// An attribute of type Boolean. For example: "BOOL": true - /// - public bool? BOOL { get; set; } + private AttributeValue(object? value, AttributeType type) + { + _referenceValue = value; + _type = type; + _boolValue = false; + } - /// - /// An attribute of type String Set. A set of Strings. - /// - public List? SS { get; set; } + private AttributeValue(bool value) + { + _referenceValue = null; + _type = AttributeType.Bool; + _boolValue = value; + } - /// - /// An attribute of type Number Set. A set of Numbers. - /// - public List? NS { get; set; } + /// Creates an AttributeValue of type String. + public static AttributeValue String(string value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.String); } - /// - /// An attribute of type Binary. DynamoDB sends binary data as a base64-encoded string. - /// - public byte[]? B { get; set; } + /// Creates an AttributeValue of type Number. + public static AttributeValue Number(string value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.Number); } - /// - /// An attribute of type Binary Set. A set of binary values, each base64-encoded. - /// - public List? BS { get; set; } + /// Creates an AttributeValue of type Bool. + public static AttributeValue Bool(bool value) => new(value); - /// - /// An attribute of type List. Lists are ordered collections of values. - /// - public List? L { get; set; } + /// Creates an AttributeValue of type Null. + public static AttributeValue Null() => new(null, AttributeType.Null); - /// - /// An attribute of type Map. Maps are unordered collections of key-value pairs. - /// - public Dictionary? M { get; set; } + /// Creates an AttributeValue from a String Set. + public static AttributeValue FromStringSet(List value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.StringSet); } + + /// Creates an AttributeValue from a Number Set. + public static AttributeValue FromNumberSet(List value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.NumberSet); } + + /// Creates an AttributeValue from a List of AttributeValues. + public static AttributeValue FromList(List value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.List); } + + /// Creates an AttributeValue from a Map of string to AttributeValue. + public static AttributeValue FromMap(Dictionary value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.Map); } + + /// Creates an AttributeValue of type Binary. + public static AttributeValue FromBinary(byte[] value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.Binary); } + + /// Creates an AttributeValue from a Binary Set. + public static AttributeValue FromBinarySet(List value) { ArgumentNullException.ThrowIfNull(value); return new(value, AttributeType.BinarySet); } /// - /// An attribute of type Null. When sending a null value, you must send the NULL attribute type. + /// An attribute of type String. Returns null if this is not a String attribute. /// - public bool? NULL { get; set; } + public string? S => _type == AttributeType.String ? (string?)_referenceValue : null; /// - /// Implicitly converts a string to an AttributeValue with S type. + /// An attribute of type Number. Returns null if this is not a Number attribute. /// - public static implicit operator AttributeValue(string value) => new() { S = value }; + public string? N => _type == AttributeType.Number ? (string?)_referenceValue : null; /// - /// Implicitly converts an int to an AttributeValue with N type. + /// An attribute of type Boolean. Returns null if this is not a Bool attribute. /// - public static implicit operator AttributeValue(int value) => new() { N = value.ToString() }; + public bool? BOOL => _type == AttributeType.Bool ? _boolValue : null; /// - /// Implicitly converts a long to an AttributeValue with N type. + /// An attribute of type Binary. Returns null if this is not a Binary attribute. /// - public static implicit operator AttributeValue(long value) => new() { N = value.ToString() }; + public byte[]? B => _type == AttributeType.Binary ? (byte[]?)_referenceValue : null; /// - /// Implicitly converts a double to an AttributeValue with N type. + /// An attribute of type Binary Set. Returns null if this is not a BinarySet attribute. /// - public static implicit operator AttributeValue(double value) => new() { N = value.ToString(CultureInfo.InvariantCulture) }; + public List? BS => _type == AttributeType.BinarySet ? (List?)_referenceValue : null; /// - /// Implicitly converts a decimal to an AttributeValue with N type. + /// An attribute of type Null. Returns true if this is a Null attribute, null otherwise. /// - public static implicit operator AttributeValue(decimal value) => new() { N = value.ToString(CultureInfo.InvariantCulture) }; + public bool? NULL => _type == AttributeType.Null ? true : null; /// - /// Implicitly converts a bool to an AttributeValue with BOOL type. + /// An attribute of type String Set. Returns null if this is not a StringSet attribute. /// - public static implicit operator AttributeValue(bool value) => new() { BOOL = value }; + public List? SS => _type == AttributeType.StringSet ? (List?)_referenceValue : null; /// - /// Implicitly converts a byte array to an AttributeValue with B type. + /// An attribute of type Number Set. Returns null if this is not a NumberSet attribute. /// - public static implicit operator AttributeValue(byte[] value) => new() { B = value }; + public List? NS => _type == AttributeType.NumberSet ? (List?)_referenceValue : null; /// - /// Implicitly converts a List of byte arrays to an AttributeValue with BS type. + /// An attribute of type List. Returns null if this is not a List attribute. /// - public static implicit operator AttributeValue(List value) => new() { BS = value }; + public List? L => _type == AttributeType.List ? (List?)_referenceValue : null; /// - /// Implicitly converts a List<string> to an AttributeValue with SS type. + /// An attribute of type Map. Returns null if this is not a Map attribute. /// - public static implicit operator AttributeValue(List value) => new() { SS = value }; + public Dictionary? M => _type == AttributeType.Map ? (Dictionary?)_referenceValue : null; + + /// Implicitly converts a string to an AttributeValue with S type. + public static implicit operator AttributeValue(string value) => String(value); + + /// Implicitly converts an int to an AttributeValue with N type. + public static implicit operator AttributeValue(int value) => Number(value.ToString()); + + /// Implicitly converts a long to an AttributeValue with N type. + public static implicit operator AttributeValue(long value) => Number(value.ToString()); + + /// Implicitly converts a double to an AttributeValue with N type. + public static implicit operator AttributeValue(double value) => Number(value.ToString(CultureInfo.InvariantCulture)); + + /// Implicitly converts a decimal to an AttributeValue with N type. + public static implicit operator AttributeValue(decimal value) => Number(value.ToString(CultureInfo.InvariantCulture)); + + /// Implicitly converts a bool to an AttributeValue with BOOL type. + public static implicit operator AttributeValue(bool value) => Bool(value); + + /// Implicitly converts a byte array to an AttributeValue with B type. + public static implicit operator AttributeValue(byte[] value) => FromBinary(value); + + /// Implicitly converts a List of byte arrays to an AttributeValue with BS type. + public static implicit operator AttributeValue(List value) => FromBinarySet(value); + + /// Implicitly converts a List<string> to an AttributeValue with SS type. + public static implicit operator AttributeValue(List value) => FromStringSet(value); /// /// Converts the AttributeValue to a strongly-typed value of the specified type. @@ -111,15 +150,15 @@ public class AttributeValue /// The converted value, or default(T) if conversion fails or the value is NULL. public T? ToValue() { - if (NULL.GetValueOrDefault()) return default; + if (_type == AttributeType.Null) return default; return typeof(T) switch { var t when t == typeof(string) => (T?)(object?)S, var t when t == typeof(int) => int.TryParse(N, out var i) ? (T?)(object?)i : default, var t when t == typeof(long) => long.TryParse(N, out var l) ? (T?)(object?)l : default, - var t when t == typeof(double) => double.TryParse(N, out var d) ? (T?)(object?)d : default, - var t when t == typeof(decimal) => decimal.TryParse(N, out var m) ? (T?)(object?)m : default, + var t when t == typeof(double) => double.TryParse(N, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? (T?)(object?)d : default, + var t when t == typeof(decimal) => decimal.TryParse(N, NumberStyles.Any, CultureInfo.InvariantCulture, out var m) ? (T?)(object?)m : default, var t when t == typeof(bool) => (T?)(object?)BOOL, var t when t == typeof(List) => (T?)(object?)SS, var t when t == typeof(byte[]) => (T?)(object?)B, @@ -128,3 +167,182 @@ public class AttributeValue }; } } + +/// +/// JSON converter for AttributeValue that handles the DynamoDB JSON format. +/// +public sealed class AttributeValueJsonConverter : JsonConverter +{ + /// + public override AttributeValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException($"Invalid AttributeValue JSON: expected StartObject but got {reader.TokenType}"); + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType != JsonTokenType.PropertyName) + continue; + + if (reader.ValueTextEquals("S"u8)) + { + reader.Read(); + var v = reader.GetString()!; + ReadEndObject(ref reader); + return AttributeValue.String(v); + } + if (reader.ValueTextEquals("N"u8)) + { + reader.Read(); + var v = reader.GetString()!; + ReadEndObject(ref reader); + return AttributeValue.Number(v); + } + if (reader.ValueTextEquals("BOOL"u8)) + { + reader.Read(); + var v = reader.GetBoolean(); + ReadEndObject(ref reader); + return AttributeValue.Bool(v); + } + if (reader.ValueTextEquals("NULL"u8)) + { + reader.Read(); + var isNull = reader.GetBoolean(); + ReadEndObject(ref reader); + if (!isNull) + throw new JsonException("Invalid NULL attribute value: expected true but got false"); + return AttributeValue.Null(); + } + if (reader.ValueTextEquals("SS"u8)) + { + reader.Read(); + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + list.Add(reader.GetString()!); + ReadEndObject(ref reader); + return AttributeValue.FromStringSet(list); + } + if (reader.ValueTextEquals("NS"u8)) + { + reader.Read(); + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + list.Add(reader.GetString()!); + ReadEndObject(ref reader); + return AttributeValue.FromNumberSet(list); + } + if (reader.ValueTextEquals("L"u8)) + { + reader.Read(); + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + list.Add(Read(ref reader, typeToConvert, options)); + ReadEndObject(ref reader); + return AttributeValue.FromList(list); + } + if (reader.ValueTextEquals("M"u8)) + { + reader.Read(); + var map = new Dictionary(8); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + var key = reader.GetString()!; + reader.Read(); + map[key] = Read(ref reader, typeToConvert, options); + } + ReadEndObject(ref reader); + return AttributeValue.FromMap(map); + } + if (reader.ValueTextEquals("B"u8)) + { + reader.Read(); + var v = Convert.FromBase64String(reader.GetString()!); + ReadEndObject(ref reader); + return AttributeValue.FromBinary(v); + } + if (reader.ValueTextEquals("BS"u8)) + { + reader.Read(); + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + list.Add(Convert.FromBase64String(reader.GetString()!)); + ReadEndObject(ref reader); + return AttributeValue.FromBinarySet(list); + } + + // Skip unknown properties + reader.Read(); + reader.Skip(); + } + + throw new JsonException("Invalid AttributeValue JSON: no recognized DynamoDB type property found"); + } + + private static void ReadEndObject(ref Utf8JsonReader reader) + { + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + throw new JsonException("Expected end of attribute value wrapper object"); + } + + /// + public override void Write(Utf8JsonWriter writer, AttributeValue value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + switch (value.Type) + { + case AttributeType.String: + writer.WriteString("S"u8, value.S); + break; + case AttributeType.Number: + writer.WriteString("N"u8, value.N); + break; + case AttributeType.Bool: + writer.WriteBoolean("BOOL"u8, value.BOOL.GetValueOrDefault()); + break; + case AttributeType.Null: + writer.WriteBoolean("NULL"u8, true); + break; + case AttributeType.StringSet: + writer.WriteStartArray("SS"u8); + foreach (var s in value.SS!) + writer.WriteStringValue(s); + writer.WriteEndArray(); + break; + case AttributeType.NumberSet: + writer.WriteStartArray("NS"u8); + foreach (var s in value.NS!) + writer.WriteStringValue(s); + writer.WriteEndArray(); + break; + case AttributeType.List: + writer.WriteStartArray("L"u8); + foreach (var item in value.L!) + Write(writer, item, options); + writer.WriteEndArray(); + break; + case AttributeType.Map: + writer.WriteStartObject("M"u8); + foreach (var kvp in value.M!) + { + writer.WritePropertyName(kvp.Key); + Write(writer, kvp.Value, options); + } + writer.WriteEndObject(); + break; + case AttributeType.Binary: + writer.WriteBase64String("B"u8, value.B); + break; + case AttributeType.BinarySet: + writer.WriteStartArray("BS"u8); + foreach (var b in value.BS!) + writer.WriteBase64StringValue(b); + writer.WriteEndArray(); + break; + } + + writer.WriteEndObject(); + } +} diff --git a/src/Clients/Goa.Clients.Dynamo/Models/DynamoRecord.cs b/src/Clients/Goa.Clients.Dynamo/Models/DynamoRecord.cs index 269d9fd5..71a9ea12 100644 --- a/src/Clients/Goa.Clients.Dynamo/Models/DynamoRecord.cs +++ b/src/Clients/Goa.Clients.Dynamo/Models/DynamoRecord.cs @@ -1,4 +1,4 @@ -namespace Goa.Clients.Dynamo.Models; +namespace Goa.Clients.Dynamo.Models; /// /// Represents a DynamoDB record with strongly-typed attribute values. @@ -8,7 +8,15 @@ public class DynamoRecord : Dictionary /// /// Initializes a new, empty instance of the class. /// - public DynamoRecord() + public DynamoRecord() : base(StringComparer.Ordinal) + { + } + + /// + /// Initializes a new instance of the class with the specified capacity. + /// + /// The initial capacity of the record. + public DynamoRecord(int capacity) : base(capacity, StringComparer.Ordinal) { } @@ -17,7 +25,7 @@ public DynamoRecord() /// by copying all key-value pairs from the specified dictionary. /// /// A dictionary containing DynamoDB attribute names and their values. - public DynamoRecord(Dictionary record) + public DynamoRecord(Dictionary record) : base(StringComparer.Ordinal) { foreach (var item in record) this[item.Key] = item.Value; @@ -30,13 +38,13 @@ public DynamoRecord(Dictionary record) /// The attribute value or null if not found. public new AttributeValue? this[string attributeName] { - get => base.TryGetValue(attributeName, out var attributeValue) ? attributeValue : null; + get => base.TryGetValue(attributeName, out var attributeValue) ? (AttributeValue?)attributeValue : null; set { - if (value == null) + if (value is null) Remove(attributeName); else - base[attributeName] = value; + base[attributeName] = value.Value; } } } diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetTableBuilder.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetTableBuilder.cs index c5373358..64f66bcc 100644 --- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetTableBuilder.cs +++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetTableBuilder.cs @@ -33,7 +33,7 @@ public BatchGetTableBuilder WithKey(string attributeName, string value) { _request.Keys.Add(new Dictionary { - [attributeName] = new AttributeValue { S = value } + [attributeName] = AttributeValue.String(value) }); return this; } diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Condition.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Condition.cs index 445abca4..8b304601 100644 --- a/src/Clients/Goa.Clients.Dynamo/Operations/Condition.cs +++ b/src/Clients/Goa.Clients.Dynamo/Operations/Condition.cs @@ -176,7 +176,7 @@ public static Condition NotContains(string attributeName, AttributeValue value) public static Condition SizeEquals(string attributeName, int size) { return new Condition($"size(#{attributeName}) = :{attributeName}", [new KeyValuePair($"#{attributeName}", attributeName) - ], [new KeyValuePair($":{attributeName}", new AttributeValue { N = size.ToString() })]); + ], [new KeyValuePair($":{attributeName}", AttributeValue.Number(size.ToString()))]); } /// @@ -188,7 +188,7 @@ public static Condition SizeEquals(string attributeName, int size) public static Condition SizeNotEquals(string attributeName, int size) { return new Condition($"size(#{attributeName}) <> :{attributeName}", [new KeyValuePair($"#{attributeName}", attributeName) - ], [new KeyValuePair($":{attributeName}", new AttributeValue { N = size.ToString() })]); + ], [new KeyValuePair($":{attributeName}", AttributeValue.Number(size.ToString()))]); } /// @@ -200,7 +200,7 @@ public static Condition SizeNotEquals(string attributeName, int size) public static Condition SizeGreaterThan(string attributeName, int size) { return new Condition($"size(#{attributeName}) > :{attributeName}", [new KeyValuePair($"#{attributeName}", attributeName) - ], [new KeyValuePair($":{attributeName}", new AttributeValue { N = size.ToString() })]); + ], [new KeyValuePair($":{attributeName}", AttributeValue.Number(size.ToString()))]); } /// @@ -212,7 +212,7 @@ public static Condition SizeGreaterThan(string attributeName, int size) public static Condition SizeGreaterThanOrEquals(string attributeName, int size) { return new Condition($"size(#{attributeName}) >= :{attributeName}", [new KeyValuePair($"#{attributeName}", attributeName) - ], [new KeyValuePair($":{attributeName}", new AttributeValue { N = size.ToString() })]); + ], [new KeyValuePair($":{attributeName}", AttributeValue.Number(size.ToString()))]); } /// @@ -224,7 +224,7 @@ public static Condition SizeGreaterThanOrEquals(string attributeName, int size) public static Condition SizeLessThan(string attributeName, int size) { return new Condition($"size(#{attributeName}) < :{attributeName}", [new KeyValuePair($"#{attributeName}", attributeName) - ], [new KeyValuePair($":{attributeName}", new AttributeValue { N = size.ToString() })]); + ], [new KeyValuePair($":{attributeName}", AttributeValue.Number(size.ToString()))]); } /// @@ -236,7 +236,7 @@ public static Condition SizeLessThan(string attributeName, int size) public static Condition SizeLessThanOrEquals(string attributeName, int size) { return new Condition($"size(#{attributeName}) <= :{attributeName}", [new KeyValuePair($"#{attributeName}", attributeName) - ], [new KeyValuePair($":{attributeName}", new AttributeValue { N = size.ToString() })]); + ], [new KeyValuePair($":{attributeName}", AttributeValue.Number(size.ToString()))]); } /// @@ -270,7 +270,7 @@ public static Condition AttributeNotExists(string attributeName) public static Condition AttributeType(string attributeName, string type) { return new Condition($"attribute_type(#{attributeName}, :{attributeName})", [new KeyValuePair($"#{attributeName}", attributeName) - ], [new KeyValuePair($":{attributeName}", new AttributeValue { S = type })]); + ], [new KeyValuePair($":{attributeName}", AttributeValue.String(type))]); } /// diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemBuilder.cs b/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemBuilder.cs index b307c75a..76ae1e43 100644 --- a/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemBuilder.cs +++ b/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemBuilder.cs @@ -266,7 +266,7 @@ private UpdateItemBuilder IncrementInternal(string attributeName, string valueSt var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue { N = valueString }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.Number(valueString); _setActions.Add($"{nameKey} = {nameKey} + {valueKey}"); return this; @@ -347,7 +347,7 @@ private UpdateItemBuilder DecrementInternal(string attributeName, string valueSt var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue { N = valueString }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.Number(valueString); _setActions.Add($"{nameKey} = {nameKey} - {valueKey}"); return this; @@ -428,7 +428,7 @@ private UpdateItemBuilder IncrementAtomicallyInternal(string attributeName, stri var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue { N = valueString }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.Number(valueString); _addActions.Add($"{nameKey} {valueKey}"); return this; @@ -509,7 +509,7 @@ private UpdateItemBuilder DecrementAtomicallyInternal(string attributeName, stri var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue { N = negatedValueString }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.Number(negatedValueString); _addActions.Add($"{nameKey} {valueKey}"); return this; @@ -663,7 +663,7 @@ public UpdateItemBuilder AddToStringSet(string attributeName, params string[] va var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue { SS = values.ToList() }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.FromStringSet(values.ToList()); _addActions.Add($"{nameKey} {valueKey}"); } @@ -687,10 +687,7 @@ public UpdateItemBuilder AddToNumberSet(string attributeName, params decimal[] v var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue - { - NS = values.Select(v => v.ToString(CultureInfo.InvariantCulture)).ToList() - }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.FromNumberSet(values.Select(v => v.ToString(CultureInfo.InvariantCulture)).ToList()); _addActions.Add($"{nameKey} {valueKey}"); } @@ -757,7 +754,7 @@ public UpdateItemBuilder RemoveFromStringSet(string attributeName, params string var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue { SS = values.ToList() }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.FromStringSet(values.ToList()); _deleteActions.Add($"{nameKey} {valueKey}"); } @@ -781,10 +778,7 @@ public UpdateItemBuilder RemoveFromNumberSet(string attributeName, params decima var valueKey = $":v{_request.ExpressionAttributeValues.Count}"; _request.ExpressionAttributeNames[nameKey] = attributeName; - _request.ExpressionAttributeValues[valueKey] = new AttributeValue - { - NS = values.Select(v => v.ToString(CultureInfo.InvariantCulture)).ToList() - }; + _request.ExpressionAttributeValues[valueKey] = AttributeValue.FromNumberSet(values.Select(v => v.ToString(CultureInfo.InvariantCulture)).ToList()); _deleteActions.Add($"{nameKey} {valueKey}"); } diff --git a/tests/Clients/Goa.Clients.Bedrock.Conversation.Dynamo.Tests/ContentBlockSerializerTests.cs b/tests/Clients/Goa.Clients.Bedrock.Conversation.Dynamo.Tests/ContentBlockSerializerTests.cs index e70bbe1d..61f6315f 100644 --- a/tests/Clients/Goa.Clients.Bedrock.Conversation.Dynamo.Tests/ContentBlockSerializerTests.cs +++ b/tests/Clients/Goa.Clients.Bedrock.Conversation.Dynamo.Tests/ContentBlockSerializerTests.cs @@ -180,14 +180,11 @@ public async Task Serialize_EmptyBlock_ShouldReturnError() [Test] public async Task Deserialize_TextBlock_ShouldDeserializeCorrectly() { - var attributeValue = new AttributeValue + var attributeValue = AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - ["type"] = new() { S = "text" }, - ["text"] = new() { S = "Hello, world!" } - } - }; + ["type"] = AttributeValue.String("text"), + ["text"] = AttributeValue.String("Hello, world!") + }); var result = ContentBlockSerializer.Deserialize(attributeValue); @@ -198,16 +195,13 @@ public async Task Deserialize_TextBlock_ShouldDeserializeCorrectly() [Test] public async Task Deserialize_ImageBlock_ShouldDeserializeCorrectly() { - var attributeValue = new AttributeValue + var attributeValue = AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - ["type"] = new() { S = "image" }, - ["format"] = new() { S = "png" }, - ["s3Uri"] = new() { S = "s3://bucket/image.png" }, - ["s3BucketOwner"] = new() { S = "123456789012" } - } - }; + ["type"] = AttributeValue.String("image"), + ["format"] = AttributeValue.String("png"), + ["s3Uri"] = AttributeValue.String("s3://bucket/image.png"), + ["s3BucketOwner"] = AttributeValue.String("123456789012") + }); var result = ContentBlockSerializer.Deserialize(attributeValue); @@ -222,16 +216,13 @@ public async Task Deserialize_ImageBlock_ShouldDeserializeCorrectly() [Test] public async Task Deserialize_DocumentBlock_ShouldDeserializeCorrectly() { - var attributeValue = new AttributeValue + var attributeValue = AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - ["type"] = new() { S = "document" }, - ["format"] = new() { S = "pdf" }, - ["name"] = new() { S = "document.pdf" }, - ["s3Uri"] = new() { S = "s3://bucket/document.pdf" } - } - }; + ["type"] = AttributeValue.String("document"), + ["format"] = AttributeValue.String("pdf"), + ["name"] = AttributeValue.String("document.pdf"), + ["s3Uri"] = AttributeValue.String("s3://bucket/document.pdf") + }); var result = ContentBlockSerializer.Deserialize(attributeValue); @@ -245,16 +236,13 @@ public async Task Deserialize_DocumentBlock_ShouldDeserializeCorrectly() [Test] public async Task Deserialize_ToolUseBlock_ShouldDeserializeCorrectly() { - var attributeValue = new AttributeValue + var attributeValue = AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - ["type"] = new() { S = "toolUse" }, - ["toolUseId"] = new() { S = "tool-123" }, - ["name"] = new() { S = "search" }, - ["input"] = new() { S = "{\"query\":\"test\"}" } - } - }; + ["type"] = AttributeValue.String("toolUse"), + ["toolUseId"] = AttributeValue.String("tool-123"), + ["name"] = AttributeValue.String("search"), + ["input"] = AttributeValue.String("{\"query\":\"test\"}") + }); var result = ContentBlockSerializer.Deserialize(attributeValue); @@ -268,29 +256,20 @@ public async Task Deserialize_ToolUseBlock_ShouldDeserializeCorrectly() [Test] public async Task Deserialize_ToolResultBlock_ShouldDeserializeCorrectly() { - var attributeValue = new AttributeValue + var attributeValue = AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - ["type"] = new() { S = "toolResult" }, - ["toolUseId"] = new() { S = "tool-123" }, - ["status"] = new() { S = "success" }, - ["content"] = new() + ["type"] = AttributeValue.String("toolResult"), + ["toolUseId"] = AttributeValue.String("tool-123"), + ["status"] = AttributeValue.String("success"), + ["content"] = AttributeValue.FromList( + [ + AttributeValue.FromMap(new Dictionary { - L = - [ - new AttributeValue - { - M = new Dictionary - { - ["type"] = new() { S = "text" }, - ["text"] = new() { S = "Result" } - } - } - ] - } - } - }; + ["type"] = AttributeValue.String("text"), + ["text"] = AttributeValue.String("Result") + }) + ]) + }); var result = ContentBlockSerializer.Deserialize(attributeValue); @@ -305,7 +284,7 @@ public async Task Deserialize_ToolResultBlock_ShouldDeserializeCorrectly() [Test] public async Task Deserialize_InvalidFormat_ShouldReturnError() { - var attributeValue = new AttributeValue { S = "not a map" }; + var attributeValue = AttributeValue.String("not a map"); var result = ContentBlockSerializer.Deserialize(attributeValue); @@ -316,13 +295,10 @@ public async Task Deserialize_InvalidFormat_ShouldReturnError() [Test] public async Task Deserialize_MissingType_ShouldReturnError() { - var attributeValue = new AttributeValue + var attributeValue = AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - ["text"] = new() { S = "Hello" } - } - }; + ["text"] = AttributeValue.String("Hello") + }); var result = ContentBlockSerializer.Deserialize(attributeValue); @@ -333,13 +309,10 @@ public async Task Deserialize_MissingType_ShouldReturnError() [Test] public async Task Deserialize_UnknownType_ShouldReturnError() { - var attributeValue = new AttributeValue + var attributeValue = AttributeValue.FromMap(new Dictionary { - M = new Dictionary - { - ["type"] = new() { S = "unknown" } - } - }; + ["type"] = AttributeValue.String("unknown") + }); var result = ContentBlockSerializer.Deserialize(attributeValue); diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchGetItemAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchGetItemAsyncBenchmarks.cs index 8fe886a6..465f1146 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchGetItemAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchGetItemAsyncBenchmarks.cs @@ -1,8 +1,12 @@ -using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; +using Goa.Clients.Dynamo.Operations.Batch; using GoaModels = Goa.Clients.Dynamo.Models; +using AwsBatchGetItemRequest = Amazon.DynamoDBv2.Model.BatchGetItemRequest; +using GoaBatchGetItemRequest = Goa.Clients.Dynamo.Operations.Batch.BatchGetItemRequest; +using EfficientBatchGetItemRequest = EfficientDynamoDb.Operations.BatchGetItem.BatchGetItemRequest; +using EfficientBatchGetItemResponse = EfficientDynamoDb.Operations.BatchGetItem.BatchGetItemResponse; using EfficientAttributeValue = EfficientDynamoDb.DocumentModel.AttributeValue; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -17,75 +21,127 @@ public void Setup() { _fixture = new LocalStackFixture(); _fixture.StartAsync().GetAwaiter().GetResult(); - _fixture.SeedItemsAsync("batch-get-bench", 25).GetAwaiter().GetResult(); + _fixture.SeedItemsAsync("batch-get", 25).GetAwaiter().GetResult(); } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } - [Benchmark(Baseline = true), BenchmarkCategory("Batch Get 25 Items")] - public async Task AwsSdk_BatchGetItem() + private static List> CreateAwsKeys(int count) { var keys = new List>(); - for (var i = 0; i < 25; i++) + for (var i = 0; i < count; i++) { keys.Add(new Dictionary { - ["pk"] = new("batch-get-bench"), - ["sk"] = new($"item-{i:D4}") + ["pk"] = new AttributeValue("batch-get"), + ["sk"] = new AttributeValue($"item-{i:D4}") }); } + return keys; + } - return await _fixture.AwsSdkClient.BatchGetItemAsync(new BatchGetItemRequest + private static List> CreateGoaKeys(int count) + { + var keys = new List>(); + for (var i = 0; i < count; i++) { - RequestItems = new Dictionary + keys.Add(new Dictionary { - [_fixture.TableName] = new() { Keys = keys } - } - }); + ["pk"] = GoaModels.AttributeValue.String("batch-get"), + ["sk"] = GoaModels.AttributeValue.String($"item-{i:D4}") + }); + } + return keys; } - [Benchmark, BenchmarkCategory("Batch Get 25 Items")] - public async Task Goa_BatchGetItem() + private static List> CreateEfficientKeys(int count) { - var keys = new List>(); - for (var i = 0; i < 25; i++) + var keys = new List>(); + for (var i = 0; i < count; i++) { - keys.Add(new Dictionary + keys.Add(new Dictionary { - ["pk"] = new() { S = "batch-get-bench" }, - ["sk"] = new() { S = $"item-{i:D4}" } + ["pk"] = "batch-get", + ["sk"] = $"item-{i:D4}" }); } + return keys; + } + + [Benchmark(Baseline = true), BenchmarkCategory("Batch Get 10 Items")] + public async Task AwsSdk_BatchGet_10Items() + { + return await _fixture.AwsSdkClient.BatchGetItemAsync(new AwsBatchGetItemRequest + { + RequestItems = new Dictionary + { + [_fixture.TableName] = new() { Keys = CreateAwsKeys(10) } + } + }); + } - var response = await _fixture.GoaClient.BatchGetItemAsync(new Goa.Clients.Dynamo.Operations.Batch.BatchGetItemRequest + [Benchmark, BenchmarkCategory("Batch Get 10 Items")] + public async Task Goa_BatchGet_10Items() + { + var response = await _fixture.GoaClient.BatchGetItemAsync(new GoaBatchGetItemRequest { - RequestItems = new Dictionary + RequestItems = new Dictionary { - [_fixture.TableName] = new() { Keys = keys } + [_fixture.TableName] = new() { Keys = CreateGoaKeys(10) } } }); return response.Value; } + [Benchmark, BenchmarkCategory("Batch Get 10 Items")] + public async Task Efficient_BatchGet_10Items() + { + return await _fixture.EfficientClient.BatchGetItemAsync(new EfficientBatchGetItemRequest + { + RequestItems = new Dictionary + { + [_fixture.TableName] = new() { Keys = CreateEfficientKeys(10) } + } + }); + } + + [Benchmark(Baseline = true), BenchmarkCategory("Batch Get 25 Items")] + public async Task AwsSdk_BatchGet_25Items() + { + return await _fixture.AwsSdkClient.BatchGetItemAsync(new AwsBatchGetItemRequest + { + RequestItems = new Dictionary + { + [_fixture.TableName] = new() { Keys = CreateAwsKeys(25) } + } + }); + } + [Benchmark, BenchmarkCategory("Batch Get 25 Items")] - public async Task Efficient_BatchGetItem() + public async Task Goa_BatchGet_25Items() { - var keys = new List>(); - for (var i = 0; i < 25; i++) + var response = await _fixture.GoaClient.BatchGetItemAsync(new GoaBatchGetItemRequest { - keys.Add(new Dictionary + RequestItems = new Dictionary { - ["pk"] = "batch-get-bench", - ["sk"] = $"item-{i:D4}" - }); - } + [_fixture.TableName] = new() { Keys = CreateGoaKeys(25) } + } + }); + return response.Value; + } - return await _fixture.EfficientClient.BatchGetItemAsync(new EfficientDynamoDb.Operations.BatchGetItem.BatchGetItemRequest + [Benchmark, BenchmarkCategory("Batch Get 25 Items")] + public async Task Efficient_BatchGet_25Items() + { + return await _fixture.EfficientClient.BatchGetItemAsync(new EfficientBatchGetItemRequest { RequestItems = new Dictionary { - [_fixture.TableName] = new() { Keys = keys } + [_fixture.TableName] = new() { Keys = CreateEfficientKeys(25) } } }); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchWriteItemAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchWriteItemAsyncBenchmarks.cs index 20cdd8f5..6657cd7d 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchWriteItemAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/BatchWriteItemAsyncBenchmarks.cs @@ -1,9 +1,15 @@ -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; using EfficientDynamoDb.DocumentModel; +using EfficientDynamoDb.Operations.BatchWriteItem; +using AwsAttributeValue = Amazon.DynamoDBv2.Model.AttributeValue; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; +using Goa.Clients.Dynamo.Operations.Batch; using GoaModels = Goa.Clients.Dynamo.Models; +using AwsBatchWriteItemRequest = Amazon.DynamoDBv2.Model.BatchWriteItemRequest; +using GoaBatchWriteItemRequest = Goa.Clients.Dynamo.Operations.Batch.BatchWriteItemRequest; +using GoaPutRequest = Goa.Clients.Dynamo.Operations.Batch.PutRequest; +using EfficientBatchWriteItemRequest = EfficientDynamoDb.Operations.BatchWriteItem.BatchWriteItemRequest; +using EfficientBatchWriteItemResponse = EfficientDynamoDb.Operations.BatchWriteItem.BatchWriteItemResponse; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -21,84 +27,161 @@ public void Setup() } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } - [Benchmark(Baseline = true), BenchmarkCategory("Batch Write 25 Items")] - public async Task AwsSdk_BatchWriteItem() + [Benchmark(Baseline = true), BenchmarkCategory("Batch Write 10 Items")] + public async Task AwsSdk_BatchWrite_10Items() { - var baseCounter = Interlocked.Add(ref _counter, 25); - var items = new List(); - for (var i = 0; i < 25; i++) + var batch = Interlocked.Increment(ref _counter); + var requests = new List(); + for (var i = 0; i < 10; i++) { - items.Add(new WriteRequest(new PutRequest(new Dictionary + requests.Add(new Amazon.DynamoDBv2.Model.WriteRequest(new Amazon.DynamoDBv2.Model.PutRequest( + new Dictionary + { + ["pk"] = new AwsAttributeValue($"aws-bw-{batch}"), + ["sk"] = new AwsAttributeValue($"item-{i:D4}"), + ["data"] = new AwsAttributeValue($"value-{i}") + }))); + } + return await _fixture.AwsSdkClient.BatchWriteItemAsync(new AwsBatchWriteItemRequest + { + RequestItems = new Dictionary> { - ["pk"] = new($"bw-aws-{baseCounter + i}"), - ["sk"] = new("item"), - ["data"] = new($"value-{baseCounter + i}") + [_fixture.TableName] = requests + } + }); + } + + [Benchmark, BenchmarkCategory("Batch Write 10 Items")] + public async Task Goa_BatchWrite_10Items() + { + var batch = Interlocked.Increment(ref _counter); + var requests = new List(); + for (var i = 0; i < 10; i++) + { + requests.Add(new BatchWriteRequestItem + { + PutRequest = new GoaPutRequest + { + Item = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String($"goa-bw-{batch}"), + ["sk"] = GoaModels.AttributeValue.String($"item-{i:D4}"), + ["data"] = GoaModels.AttributeValue.String($"value-{i}") + } + } + }); + } + var response = await _fixture.GoaClient.BatchWriteItemAsync(new GoaBatchWriteItemRequest + { + RequestItems = new Dictionary> + { + [_fixture.TableName] = requests + } + }); + return !response.IsError; + } + + [Benchmark, BenchmarkCategory("Batch Write 10 Items")] + public async Task Efficient_BatchWrite_10Items() + { + var batch = Interlocked.Increment(ref _counter); + var operations = new List(); + for (var i = 0; i < 10; i++) + { + operations.Add(new BatchWriteOperation(new BatchWritePutRequest(new Document + { + ["pk"] = $"eff-bw-{batch}", + ["sk"] = $"item-{i:D4}", + ["data"] = $"value-{i}" }))); } + return await _fixture.EfficientClient.BatchWriteItemAsync(new EfficientBatchWriteItemRequest + { + RequestItems = new Dictionary> + { + [_fixture.TableName] = operations + } + }); + } - return await _fixture.AwsSdkClient.BatchWriteItemAsync(new BatchWriteItemRequest + [Benchmark(Baseline = true), BenchmarkCategory("Batch Write 25 Items")] + public async Task AwsSdk_BatchWrite_25Items() + { + var batch = Interlocked.Increment(ref _counter); + var requests = new List(); + for (var i = 0; i < 25; i++) + { + requests.Add(new Amazon.DynamoDBv2.Model.WriteRequest(new Amazon.DynamoDBv2.Model.PutRequest( + new Dictionary + { + ["pk"] = new AwsAttributeValue($"aws-bw25-{batch}"), + ["sk"] = new AwsAttributeValue($"item-{i:D4}"), + ["data"] = new AwsAttributeValue($"value-{i}") + }))); + } + return await _fixture.AwsSdkClient.BatchWriteItemAsync(new AwsBatchWriteItemRequest { - RequestItems = new Dictionary> + RequestItems = new Dictionary> { - [_fixture.TableName] = items + [_fixture.TableName] = requests } }); } [Benchmark, BenchmarkCategory("Batch Write 25 Items")] - public async Task Goa_BatchWriteItem() + public async Task Goa_BatchWrite_25Items() { - var baseCounter = Interlocked.Add(ref _counter, 25); - var items = new List(); + var batch = Interlocked.Increment(ref _counter); + var requests = new List(); for (var i = 0; i < 25; i++) { - items.Add(new Goa.Clients.Dynamo.Operations.Batch.BatchWriteRequestItem + requests.Add(new BatchWriteRequestItem { - PutRequest = new Goa.Clients.Dynamo.Operations.Batch.PutRequest + PutRequest = new GoaPutRequest { Item = new Dictionary { - ["pk"] = new() { S = $"bw-goa-{baseCounter + i}" }, - ["sk"] = new() { S = "item" }, - ["data"] = new() { S = $"value-{baseCounter + i}" } + ["pk"] = GoaModels.AttributeValue.String($"goa-bw25-{batch}"), + ["sk"] = GoaModels.AttributeValue.String($"item-{i:D4}"), + ["data"] = GoaModels.AttributeValue.String($"value-{i}") } } }); } - - var response = await _fixture.GoaClient.BatchWriteItemAsync(new Goa.Clients.Dynamo.Operations.Batch.BatchWriteItemRequest + var response = await _fixture.GoaClient.BatchWriteItemAsync(new GoaBatchWriteItemRequest { - RequestItems = new Dictionary> + RequestItems = new Dictionary> { - [_fixture.TableName] = items + [_fixture.TableName] = requests } }); - return response.Value; + return !response.IsError; } [Benchmark, BenchmarkCategory("Batch Write 25 Items")] - public async Task Efficient_BatchWriteItem() + public async Task Efficient_BatchWrite_25Items() { - var baseCounter = Interlocked.Add(ref _counter, 25); - var items = new List(); + var batch = Interlocked.Increment(ref _counter); + var operations = new List(); for (var i = 0; i < 25; i++) { - items.Add(new EfficientDynamoDb.Operations.BatchWriteItem.BatchWriteOperation( - new EfficientDynamoDb.Operations.BatchWriteItem.BatchWritePutRequest(new Document - { - ["pk"] = $"bw-eff-{baseCounter + i}", - ["sk"] = "item", - ["data"] = $"value-{baseCounter + i}" - }))); + operations.Add(new BatchWriteOperation(new BatchWritePutRequest(new Document + { + ["pk"] = $"eff-bw25-{batch}", + ["sk"] = $"item-{i:D4}", + ["data"] = $"value-{i}" + }))); } - - return await _fixture.EfficientClient.BatchWriteItemAsync(new EfficientDynamoDb.Operations.BatchWriteItem.BatchWriteItemRequest + return await _fixture.EfficientClient.BatchWriteItemAsync(new EfficientBatchWriteItemRequest { - RequestItems = new Dictionary> + RequestItems = new Dictionary> { - [_fixture.TableName] = items + [_fixture.TableName] = operations } }); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/DeleteItemAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/DeleteItemAsyncBenchmarks.cs index 847bcd47..b9fb5b14 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/DeleteItemAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/DeleteItemAsyncBenchmarks.cs @@ -4,6 +4,7 @@ using EfficientDynamoDb.Operations.Shared; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; using GoaModels = Goa.Clients.Dynamo.Models; +using EfficientDeleteItemRequest = EfficientDynamoDb.Operations.DeleteItem.DeleteItemRequest; using EfficientDeleteItemResponse = EfficientDynamoDb.Operations.DeleteItem.DeleteItemResponse; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -11,7 +12,6 @@ namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; [MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), Orderer(SummaryOrderPolicy.FastestToSlowest)] public class DeleteItemAsyncBenchmarks { - private const int PoolSize = 10_000; private LocalStackFixture _fixture = null!; private int _counter; @@ -20,53 +20,99 @@ public void Setup() { _fixture = new LocalStackFixture(); _fixture.StartAsync().GetAwaiter().GetResult(); - _fixture.SeedItemsByPkPrefixAsync("del-aws-", "item", PoolSize).GetAwaiter().GetResult(); - _fixture.SeedItemsByPkPrefixAsync("del-goa-", "item", PoolSize).GetAwaiter().GetResult(); - _fixture.SeedItemsByPkPrefixAsync("del-eff-", "item", PoolSize).GetAwaiter().GetResult(); } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } [Benchmark(Baseline = true), BenchmarkCategory("Delete Item")] public async Task AwsSdk_DeleteItem() { - var i = Interlocked.Increment(ref _counter); + // Delete a non-existent item (no-op but exercises full request path) + var id = Interlocked.Increment(ref _counter); return await _fixture.AwsSdkClient.DeleteItemAsync(new DeleteItemRequest { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new($"del-aws-{i}"), - ["sk"] = new("item") + ["pk"] = new AttributeValue($"del-aws-{id}"), + ["sk"] = new AttributeValue("item") } }); } [Benchmark, BenchmarkCategory("Delete Item")] - public async Task Goa_DeleteItem() + public async Task Goa_DeleteItem() { - var i = Interlocked.Increment(ref _counter); + var id = Interlocked.Increment(ref _counter); var response = await _fixture.GoaClient.DeleteItemAsync(new Goa.Clients.Dynamo.Operations.DeleteItem.DeleteItemRequest { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new() { S = $"del-goa-{i}" }, - ["sk"] = new() { S = "item" } + ["pk"] = GoaModels.AttributeValue.String($"del-goa-{id}"), + ["sk"] = GoaModels.AttributeValue.String("item") } }); - return response.Value; + return !response.IsError; } [Benchmark, BenchmarkCategory("Delete Item")] public async Task Efficient_DeleteItem() { - var i = Interlocked.Increment(ref _counter); - return await _fixture.EfficientClient.DeleteItemAsync(new EfficientDynamoDb.Operations.DeleteItem.DeleteItemRequest + var id = Interlocked.Increment(ref _counter); + return await _fixture.EfficientClient.DeleteItemAsync(new EfficientDeleteItemRequest + { + TableName = _fixture.TableName, + Key = new PrimaryKey("pk", $"del-eff-{id}", "sk", "item") + }); + } + + [Benchmark(Baseline = true), BenchmarkCategory("Delete Item With Return Values")] + public async Task AwsSdk_DeleteItem_WithReturnValues() + { + var id = Interlocked.Increment(ref _counter); + return await _fixture.AwsSdkClient.DeleteItemAsync(new DeleteItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = new AttributeValue($"delret-aws-{id}"), + ["sk"] = new AttributeValue("item") + }, + ReturnValues = ReturnValue.ALL_OLD + }); + } + + [Benchmark, BenchmarkCategory("Delete Item With Return Values")] + public async Task Goa_DeleteItem_WithReturnValues() + { + var id = Interlocked.Increment(ref _counter); + var response = await _fixture.GoaClient.DeleteItemAsync(new Goa.Clients.Dynamo.Operations.DeleteItem.DeleteItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String($"delret-goa-{id}"), + ["sk"] = GoaModels.AttributeValue.String("item") + }, + ReturnValues = Goa.Clients.Dynamo.Enums.ReturnValues.ALL_OLD + }); + return response.Value.Attributes; + } + + [Benchmark, BenchmarkCategory("Delete Item With Return Values")] + public async Task Efficient_DeleteItem_WithReturnValues() + { + var id = Interlocked.Increment(ref _counter); + return await _fixture.EfficientClient.DeleteItemAsync(new EfficientDeleteItemRequest { TableName = _fixture.TableName, - Key = new PrimaryKey("pk", $"del-eff-{i}", "sk", "item") + Key = new PrimaryKey("pk", $"delret-eff-{id}", "sk", "item"), + ReturnValues = ReturnValues.AllOld }); } } diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/GetItemAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/GetItemAsyncBenchmarks.cs index 14bfe278..b559ea55 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/GetItemAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/GetItemAsyncBenchmarks.cs @@ -1,8 +1,8 @@ -using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; using EfficientDynamoDb.Operations.Shared; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; + using GoaModels = Goa.Clients.Dynamo.Models; using EfficientGetItemResponse = EfficientDynamoDb.Operations.GetItem.GetItemResponse; @@ -18,11 +18,16 @@ public void Setup() { _fixture = new LocalStackFixture(); _fixture.StartAsync().GetAwaiter().GetResult(); + + // Seed one item for "hit" benchmarks _fixture.SeedItemsAsync("get-bench", 1).GetAwaiter().GetResult(); } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } [Benchmark(Baseline = true), BenchmarkCategory("Get Item")] public async Task AwsSdk_GetItem() @@ -32,27 +37,37 @@ public async Task AwsSdk_GetItem() TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new("get-bench"), - ["sk"] = new("item-0000") + ["pk"] = new AttributeValue("get-bench"), + ["sk"] = new AttributeValue("item-0000") } }); } [Benchmark, BenchmarkCategory("Get Item")] - public async Task Goa_GetItem() + public async Task Goa_GetItem_DynamoRecord() { var response = await _fixture.GoaClient.GetItemAsync(new Goa.Clients.Dynamo.Operations.GetItem.GetItemRequest { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new() { S = "get-bench" }, - ["sk"] = new() { S = "item-0000" } + ["pk"] = GoaModels.AttributeValue.String("get-bench"), + ["sk"] = GoaModels.AttributeValue.String("item-0000") } }); return response.Value.Item; } + [Benchmark, BenchmarkCategory("Get Item")] + public async Task Efficient_GetItem() + { + return await _fixture.EfficientClient.GetItemAsync(new EfficientDynamoDb.Operations.GetItem.GetItemRequest + { + TableName = _fixture.TableName, + Key = new PrimaryKey("pk", "get-bench", "sk", "item-0000") + }); + } + [Benchmark(Baseline = true), BenchmarkCategory("Get Item Miss")] public async Task AwsSdk_GetItem_Miss() { @@ -61,44 +76,76 @@ public async Task AwsSdk_GetItem_Miss() TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new("nonexistent"), - ["sk"] = new("nonexistent") + ["pk"] = new AttributeValue("nonexistent"), + ["sk"] = new AttributeValue("nonexistent") } }); } [Benchmark, BenchmarkCategory("Get Item Miss")] - public async Task Goa_GetItem_Miss() + public async Task Goa_GetItem_Miss_DynamoRecord() { var response = await _fixture.GoaClient.GetItemAsync(new Goa.Clients.Dynamo.Operations.GetItem.GetItemRequest { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new() { S = "nonexistent" }, - ["sk"] = new() { S = "nonexistent" } + ["pk"] = GoaModels.AttributeValue.String("nonexistent"), + ["sk"] = GoaModels.AttributeValue.String("nonexistent") } }); return response.Value.Item; } - [Benchmark, BenchmarkCategory("Get Item")] - public async Task Efficient_GetItem() + [Benchmark, BenchmarkCategory("Get Item Miss")] + public async Task Efficient_GetItem_Miss() { return await _fixture.EfficientClient.GetItemAsync(new EfficientDynamoDb.Operations.GetItem.GetItemRequest { TableName = _fixture.TableName, - Key = new PrimaryKey("pk", "get-bench", "sk", "item-0000") + Key = new PrimaryKey("pk", "nonexistent", "sk", "nonexistent") }); } - [Benchmark, BenchmarkCategory("Get Item Miss")] - public async Task Efficient_GetItem_Miss() + [Benchmark(Baseline = true), BenchmarkCategory("Get Item Consistent Read")] + public async Task AwsSdk_GetItem_ConsistentRead() + { + return await _fixture.AwsSdkClient.GetItemAsync(new GetItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = new AttributeValue("get-bench"), + ["sk"] = new AttributeValue("item-0000") + }, + ConsistentRead = true + }); + } + + [Benchmark, BenchmarkCategory("Get Item Consistent Read")] + public async Task Goa_GetItem_ConsistentRead_DynamoRecord() + { + var response = await _fixture.GoaClient.GetItemAsync(new Goa.Clients.Dynamo.Operations.GetItem.GetItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String("get-bench"), + ["sk"] = GoaModels.AttributeValue.String("item-0000") + }, + ConsistentRead = true + }); + return response.Value.Item; + } + + [Benchmark, BenchmarkCategory("Get Item Consistent Read")] + public async Task Efficient_GetItem_ConsistentRead() { return await _fixture.EfficientClient.GetItemAsync(new EfficientDynamoDb.Operations.GetItem.GetItemRequest { TableName = _fixture.TableName, - Key = new PrimaryKey("pk", "nonexistent", "sk", "nonexistent") + Key = new PrimaryKey("pk", "get-bench", "sk", "item-0000"), + ConsistentRead = true }); } } diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/PutItemAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/PutItemAsyncBenchmarks.cs index f95f7f50..e17f3341 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/PutItemAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/PutItemAsyncBenchmarks.cs @@ -1,9 +1,9 @@ -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; using EfficientDynamoDb.DocumentModel; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; using GoaModels = Goa.Clients.Dynamo.Models; +using AwsAttributeValue = Amazon.DynamoDBv2.Model.AttributeValue; +using EfficientPutItemRequest = EfficientDynamoDb.Operations.PutItem.PutItemRequest; using EfficientPutItemResponse = EfficientDynamoDb.Operations.PutItem.PutItemResponse; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -22,59 +22,214 @@ public void Setup() } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true), BenchmarkCategory("Put Item Simple")] + public async Task AwsSdk_PutItem_Simple() + { + var id = Interlocked.Increment(ref _counter); + return await _fixture.AwsSdkClient.PutItemAsync(new Amazon.DynamoDBv2.Model.PutItemRequest + { + TableName = _fixture.TableName, + Item = new Dictionary + { + ["pk"] = new AwsAttributeValue($"aws-put-{id}"), + ["sk"] = new AwsAttributeValue("simple"), + ["data"] = new AwsAttributeValue("test-value"), + ["number"] = new AwsAttributeValue { N = "42" } + } + }); + } + + [Benchmark, BenchmarkCategory("Put Item Simple")] + public async Task Goa_PutItem_Simple() + { + var id = Interlocked.Increment(ref _counter); + var response = await _fixture.GoaClient.PutItemAsync(new Goa.Clients.Dynamo.Operations.PutItem.PutItemRequest + { + TableName = _fixture.TableName, + Item = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String($"goa-put-{id}"), + ["sk"] = GoaModels.AttributeValue.String("simple"), + ["data"] = GoaModels.AttributeValue.String("test-value"), + ["number"] = GoaModels.AttributeValue.Number("42") + } + }); + return !response.IsError; + } + + [Benchmark, BenchmarkCategory("Put Item Simple")] + public async Task Efficient_PutItem_Simple() + { + var id = Interlocked.Increment(ref _counter); + return await _fixture.EfficientClient.PutItemAsync(new EfficientPutItemRequest + { + TableName = _fixture.TableName, + Item = new Document + { + ["pk"] = $"eff-put-{id}", + ["sk"] = "simple", + ["data"] = "test-value", + ["number"] = 42 + } + }); + } + + [Benchmark(Baseline = true), BenchmarkCategory("Put Item Wide")] + public async Task AwsSdk_PutItem_WideItem() + { + var id = Interlocked.Increment(ref _counter); + var item = new Dictionary + { + ["pk"] = new AwsAttributeValue($"aws-wide-{id}"), + ["sk"] = new AwsAttributeValue("wide") + }; + for (var i = 0; i < 20; i++) + { + item[$"attr_{i}"] = new AwsAttributeValue($"value-{i}-{new string('x', 100)}"); + } + return await _fixture.AwsSdkClient.PutItemAsync(new Amazon.DynamoDBv2.Model.PutItemRequest + { + TableName = _fixture.TableName, + Item = item + }); + } + + [Benchmark, BenchmarkCategory("Put Item Wide")] + public async Task Goa_PutItem_WideItem() + { + var id = Interlocked.Increment(ref _counter); + var item = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String($"goa-wide-{id}"), + ["sk"] = GoaModels.AttributeValue.String("wide") + }; + for (var i = 0; i < 20; i++) + { + item[$"attr_{i}"] = GoaModels.AttributeValue.String($"value-{i}-{new string('x', 100)}"); + } + var response = await _fixture.GoaClient.PutItemAsync(new Goa.Clients.Dynamo.Operations.PutItem.PutItemRequest + { + TableName = _fixture.TableName, + Item = item + }); + return !response.IsError; + } + + [Benchmark, BenchmarkCategory("Put Item Wide")] + public async Task Efficient_PutItem_WideItem() + { + var id = Interlocked.Increment(ref _counter); + var item = new Document + { + ["pk"] = $"eff-wide-{id}", + ["sk"] = "wide" + }; + for (var i = 0; i < 20; i++) + { + item[$"attr_{i}"] = $"value-{i}-{new string('x', 100)}"; + } + return await _fixture.EfficientClient.PutItemAsync(new EfficientPutItemRequest + { + TableName = _fixture.TableName, + Item = item + }); + } - [Benchmark(Baseline = true), BenchmarkCategory("Put Item")] - public async Task AwsSdk_PutItem() + [Benchmark(Baseline = true), BenchmarkCategory("Put Item Complex")] + public async Task AwsSdk_PutItem_ComplexItem() { - var i = Interlocked.Increment(ref _counter); - return await _fixture.AwsSdkClient.PutItemAsync(new PutItemRequest + var id = Interlocked.Increment(ref _counter); + return await _fixture.AwsSdkClient.PutItemAsync(new Amazon.DynamoDBv2.Model.PutItemRequest { TableName = _fixture.TableName, - Item = new Dictionary + Item = new Dictionary { - ["pk"] = new($"put-aws-{i}"), - ["sk"] = new("item"), - ["data"] = new($"value-{i}"), - ["number"] = new() { N = i.ToString() }, - ["status"] = new("active") + ["pk"] = new AwsAttributeValue($"aws-complex-{id}"), + ["sk"] = new AwsAttributeValue("complex"), + ["data"] = new AwsAttributeValue("test"), + ["tags"] = new AwsAttributeValue { SS = ["tag1", "tag2", "tag3"] }, + ["scores"] = new AwsAttributeValue { NS = ["1", "2", "3"] }, + ["metadata"] = new AwsAttributeValue + { + M = new Dictionary + { + ["nested1"] = new AwsAttributeValue("val1"), + ["nested2"] = new AwsAttributeValue { N = "99" } + } + }, + ["items"] = new AwsAttributeValue + { + L = + [ + new AwsAttributeValue("listval1"), + new AwsAttributeValue { N = "42" }, + new AwsAttributeValue { BOOL = true } + ] + } } }); } - [Benchmark, BenchmarkCategory("Put Item")] - public async Task Goa_PutItem() + [Benchmark, BenchmarkCategory("Put Item Complex")] + public async Task Goa_PutItem_ComplexItem() { - var i = Interlocked.Increment(ref _counter); + var id = Interlocked.Increment(ref _counter); var response = await _fixture.GoaClient.PutItemAsync(new Goa.Clients.Dynamo.Operations.PutItem.PutItemRequest { TableName = _fixture.TableName, Item = new Dictionary { - ["pk"] = new() { S = $"put-goa-{i}" }, - ["sk"] = new() { S = "item" }, - ["data"] = new() { S = $"value-{i}" }, - ["number"] = new() { N = i.ToString() }, - ["status"] = new() { S = "active" } + ["pk"] = GoaModels.AttributeValue.String($"goa-complex-{id}"), + ["sk"] = GoaModels.AttributeValue.String("complex"), + ["data"] = GoaModels.AttributeValue.String("test"), + ["tags"] = GoaModels.AttributeValue.FromStringSet(["tag1", "tag2", "tag3"]), + ["scores"] = GoaModels.AttributeValue.FromNumberSet(["1", "2", "3"]), + ["metadata"] = GoaModels.AttributeValue.FromMap(new Dictionary + { + ["nested1"] = GoaModels.AttributeValue.String("val1"), + ["nested2"] = GoaModels.AttributeValue.Number("99") + }), + ["items"] = GoaModels.AttributeValue.FromList( + [ + GoaModels.AttributeValue.String("listval1"), + GoaModels.AttributeValue.Number("42"), + GoaModels.AttributeValue.Bool(true) + ]) } }); - return response.Value; + return !response.IsError; } - [Benchmark, BenchmarkCategory("Put Item")] - public async Task Efficient_PutItem() + [Benchmark, BenchmarkCategory("Put Item Complex")] + public async Task Efficient_PutItem_ComplexItem() { - var i = Interlocked.Increment(ref _counter); - return await _fixture.EfficientClient.PutItemAsync(new EfficientDynamoDb.Operations.PutItem.PutItemRequest + var id = Interlocked.Increment(ref _counter); + return await _fixture.EfficientClient.PutItemAsync(new EfficientPutItemRequest { TableName = _fixture.TableName, Item = new Document { - ["pk"] = $"put-eff-{i}", - ["sk"] = "item", - ["data"] = $"value-{i}", - ["number"] = i, - ["status"] = "active" + ["pk"] = $"eff-complex-{id}", + ["sk"] = "complex", + ["data"] = "test", + ["tags"] = new StringSetAttributeValue(new HashSet { "tag1", "tag2", "tag3" }), + ["scores"] = new NumberSetAttributeValue(new HashSet { "1", "2", "3" }), + ["metadata"] = new Document + { + ["nested1"] = "val1", + ["nested2"] = new NumberAttributeValue("99") + }, + ["items"] = new ListAttributeValue(new List + { + new StringAttributeValue("listval1"), + new NumberAttributeValue("42"), + new BoolAttributeValue(true) + }) } }); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs index a5b3595c..827b094c 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs @@ -1,17 +1,36 @@ -using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; using EfficientDynamoDb; +using EfficientDynamoDb.Attributes; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; -using Goa.Clients.Dynamo.Benchmarks.Models; +using EfficientAttributeValue = EfficientDynamoDb.DocumentModel.AttributeValue; using GoaModels = Goa.Clients.Dynamo.Models; using GoaQueryRequest = Goa.Clients.Dynamo.Operations.Query.QueryRequest; using EfficientQueryRequest = EfficientDynamoDb.Operations.Query.QueryRequest; -using EfficientAttributeValue = EfficientDynamoDb.DocumentModel.AttributeValue; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; -[MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), Orderer(SummaryOrderPolicy.FastestToSlowest)] +[DynamoDbTable("benchmark-table")] +public class BenchmarkEntity +{ + [DynamoDbProperty("pk", DynamoDbAttributeType.PartitionKey)] + public string Pk { get; set; } = ""; + + [DynamoDbProperty("sk", DynamoDbAttributeType.SortKey)] + public string Sk { get; set; } = ""; + + [DynamoDbProperty("data")] + public string Data { get; set; } = ""; + + [DynamoDbProperty("number")] + public int Number { get; set; } + + [DynamoDbProperty("status")] + public string Status { get; set; } = ""; +} + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), Orderer(SummaryOrderPolicy.FastestToSlowest)] public class QueryAsyncBenchmarks { private LocalStackFixture _fixture = null!; @@ -21,28 +40,233 @@ public void Setup() { _fixture = new LocalStackFixture(); _fixture.StartAsync().GetAwaiter().GetResult(); - _fixture.SeedItemsAsync("query-bench", 100).GetAwaiter().GetResult(); + + // Seed items for query benchmarks + _fixture.SeedItemsAsync("query-1", 1).GetAwaiter().GetResult(); + _fixture.SeedItemsAsync("query-10", 10).GetAwaiter().GetResult(); + _fixture.SeedItemsAsync("query-100", 100).GetAwaiter().GetResult(); } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + + // // --- Single item query --- + // + // [Benchmark(Baseline = true), BenchmarkCategory("1 Item")] + // public async Task AwsSdk_Query_1Item() + // { + // var count = 0; + // Dictionary? lastKey = null; + // do + // { + // var response = await _fixture.AwsSdkClient.QueryAsync(new Amazon.DynamoDBv2.Model.QueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = new AttributeValue("query-1") + // }, + // ExclusiveStartKey = lastKey + // }); + // count += response.Items.Count; + // lastKey = response.LastEvaluatedKey?.Count > 0 ? response.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("1 Item")] + // public async Task Goa_Query_1Item_DynamoRecord() + // { + // var count = 0; + // Dictionary? lastKey = null; + // do + // { + // var response = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = GoaModels.AttributeValue.String("query-1") + // }, + // ExclusiveStartKey = lastKey + // }); + // + // count += response.Value.Items.Count; + // lastKey = response.Value.HasMoreResults ? response.Value.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("1 Item")] + // public async Task Efficient_Query_1Item() + // { + // var count = 0; + // IReadOnlyDictionary? lastKey = null; + // do + // { + // var response = await _fixture.EfficientClient.QueryAsync(new EfficientQueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = "query-1" + // }, + // ExclusiveStartKey = lastKey + // }); + // count += response.Items.Count; + // lastKey = response.LastEvaluatedKey; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("1 Item")] + // public async Task Efficient_Query_1Item_Typed() + // { + // return (await _fixture.EfficientContext.Query() + // .WithKeyExpression(Condition.On(x => x.Pk).EqualTo("query-1")) + // .ToListAsync()).Count; + // } - [Benchmark(Baseline = true), BenchmarkCategory("Query 100 Items")] - public async Task AwsSdk_Query() + // --- 10 item query (Limit=5, forces 2 pages) --- + + // [Benchmark(Baseline = true), BenchmarkCategory("10 Items")] + // public async Task AwsSdk_Query_10Items() + // { + // var count = 0; + // Dictionary? lastKey = null; + // do + // { + // var response = await _fixture.AwsSdkClient.QueryAsync(new Amazon.DynamoDBv2.Model.QueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = new AttributeValue("query-10") + // }, + // Limit = 5, + // ExclusiveStartKey = lastKey + // }); + // count += response.Items.Count; + // lastKey = response.LastEvaluatedKey?.Count > 0 ? response.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("10 Items")] + // public async Task Goa_Query_10Items_DynamoRecord() + // { + // var count = 0; + // Dictionary? lastKey = null; + // do + // { + // var response = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = GoaModels.AttributeValue.String("query-10") + // }, + // Limit = 5, + // ExclusiveStartKey = lastKey + // }); + // count += response.Value.Items.Count; + // lastKey = response.Value.HasMoreResults ? response.Value.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("10 Items")] + // public async Task Goa_Query_10Items_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-10") + // }, + // Limit = 5, + // ExclusiveStartKey = lastKey + // }, DynamoItemReaderRegistry.Get()); + // count += result.Value.Items.Count; + // lastKey = result.Value.HasMoreResults ? result.Value.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("10 Items")] + // public async Task Efficient_Query_10Items() + // { + // var count = 0; + // IReadOnlyDictionary? lastKey = null; + // do + // { + // var response = await _fixture.EfficientClient.QueryAsync(new EfficientQueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = "query-10" + // }, + // Limit = 5, + // ExclusiveStartKey = lastKey + // }); + // count += response.Items.Count; + // lastKey = response.LastEvaluatedKey; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("10 Items")] + // public async Task Efficient_Query_10Items_Typed() + // { + // var count = 0; + // string? paginationToken = null; + // do + // { + // var page = await _fixture.EfficientContext.Query() + // .WithKeyExpression(Condition.On(x => x.Pk).EqualTo("query-10")) + // .WithLimit(5) + // .WithPaginationToken(paginationToken) + // .ToPageAsync(); + // count += page.Items.Count; + // paginationToken = page.PaginationToken; + // } while (paginationToken != null); + // return count; + // } + // + // --- 100 item query (Limit=25, forces 4 pages) --- + + [Benchmark(Baseline = true), BenchmarkCategory("100 Items")] + public async Task AwsSdk_Query_100Items() { var count = 0; Dictionary? lastKey = null; do { - var response = await _fixture.AwsSdkClient.QueryAsync(new QueryRequest + var response = await _fixture.AwsSdkClient.QueryAsync(new Amazon.DynamoDBv2.Model.QueryRequest { TableName = _fixture.TableName, KeyConditionExpression = "pk = :pk", ExpressionAttributeValues = new Dictionary { - [":pk"] = new("query-bench") + [":pk"] = new AttributeValue("query-100") }, - Limit = 20, + Limit = 25, ExclusiveStartKey = lastKey }); count += response.Items.Count; @@ -51,8 +275,8 @@ public async Task AwsSdk_Query() return count; } - [Benchmark, BenchmarkCategory("Query 100 Items")] - public async Task Goa_Query() + [Benchmark, BenchmarkCategory("100 Items")] + public async Task Goa_Query_100Items_DynamoRecord() { var count = 0; Dictionary? lastKey = null; @@ -64,9 +288,9 @@ public async Task Goa_Query() KeyConditionExpression = "pk = :pk", ExpressionAttributeValues = new Dictionary { - [":pk"] = new() { S = "query-bench" } + [":pk"] = GoaModels.AttributeValue.String("query-100") }, - Limit = 20, + Limit = 25, ExclusiveStartKey = lastKey }); count += response.Value.Items.Count; @@ -75,8 +299,32 @@ public async Task Goa_Query() return count; } - [Benchmark, BenchmarkCategory("Query 100 Items")] - public async Task Efficient_Query() + // [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() { var count = 0; IReadOnlyDictionary? lastKey = null; @@ -88,9 +336,9 @@ public async Task Efficient_Query() KeyConditionExpression = "pk = :pk", ExpressionAttributeValues = new Dictionary { - [":pk"] = "query-bench" + [":pk"] = "query-100" }, - Limit = 20, + Limit = 25, ExclusiveStartKey = lastKey }); count += response.Items.Count; @@ -99,16 +347,16 @@ public async Task Efficient_Query() return count; } - [Benchmark, BenchmarkCategory("Query 100 Items")] - public async Task Efficient_Query_Typed() + [Benchmark, BenchmarkCategory("100 Items")] + public async Task Efficient_Query_100Items_Typed() { var count = 0; string? paginationToken = null; do { var page = await _fixture.EfficientContext.Query() - .WithKeyExpression(Condition.On(x => x.Pk).EqualTo("query-bench")) - .WithLimit(20) + .WithKeyExpression(Condition.On(x => x.Pk).EqualTo("query-100")) + .WithLimit(25) .WithPaginationToken(paginationToken) .ToPageAsync(); count += page.Items.Count; @@ -116,4 +364,106 @@ public async Task Efficient_Query_Typed() } while (paginationToken != null); return count; } + + // // --- Empty query --- + // + // [Benchmark(Baseline = true), BenchmarkCategory("No Results")] + // public async Task AwsSdk_Query_NoResults() + // { + // var count = 0; + // Dictionary? lastKey = null; + // do + // { + // var response = await _fixture.AwsSdkClient.QueryAsync(new Amazon.DynamoDBv2.Model.QueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = new AttributeValue("nonexistent-pk") + // }, + // ExclusiveStartKey = lastKey + // }); + // count += response.Items.Count; + // lastKey = response.LastEvaluatedKey?.Count > 0 ? response.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("No Results")] + // public async Task Goa_Query_NoResults_DynamoRecord() + // { + // var count = 0; + // Dictionary? lastKey = null; + // do + // { + // var response = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = GoaModels.AttributeValue.String("nonexistent-pk") + // }, + // ExclusiveStartKey = lastKey + // }); + // count += response.Value.Items.Count; + // lastKey = response.Value.HasMoreResults ? response.Value.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("No Results")] + // public async Task Goa_Query_NoResults_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("nonexistent-pk") + // }, + // ExclusiveStartKey = lastKey + // }, DynamoItemReaderRegistry.Get()); + // count += result.Value.Items.Count; + // lastKey = result.Value.HasMoreResults ? result.Value.LastEvaluatedKey : null; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("No Results")] + // public async Task Efficient_Query_NoResults() + // { + // var count = 0; + // IReadOnlyDictionary? lastKey = null; + // do + // { + // var response = await _fixture.EfficientClient.QueryAsync(new EfficientQueryRequest + // { + // TableName = _fixture.TableName, + // KeyConditionExpression = "pk = :pk", + // ExpressionAttributeValues = new Dictionary + // { + // [":pk"] = "nonexistent-pk" + // }, + // ExclusiveStartKey = lastKey + // }); + // count += response.Items.Count; + // lastKey = response.LastEvaluatedKey; + // } while (lastKey != null); + // return count; + // } + // + // [Benchmark, BenchmarkCategory("No Results")] + // public async Task Efficient_Query_NoResults_Typed() + // { + // return (await _fixture.EfficientContext.Query() + // .WithKeyExpression(Condition.On(x => x.Pk).EqualTo("nonexistent-pk")) + // .ToListAsync()).Count; + // } } diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/ScanAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/ScanAsyncBenchmarks.cs index 414e2e20..07601bb3 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/ScanAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/ScanAsyncBenchmarks.cs @@ -1,11 +1,12 @@ -using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; +using EfficientDynamoDb.DocumentModel; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; using GoaModels = Goa.Clients.Dynamo.Models; using GoaScanRequest = Goa.Clients.Dynamo.Operations.Scan.ScanRequest; using EfficientScanRequest = EfficientDynamoDb.Operations.Scan.ScanRequest; using EfficientAttributeValue = EfficientDynamoDb.DocumentModel.AttributeValue; +using AwsAttributeValue = Amazon.DynamoDBv2.Model.AttributeValue; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -19,23 +20,31 @@ public void Setup() { _fixture = new LocalStackFixture(); _fixture.StartAsync().GetAwaiter().GetResult(); - _fixture.SeedItemsAsync("scan-bench", 100).GetAwaiter().GetResult(); + + // Seed items spread across different partition keys for scan + _fixture.SeedItemsAsync("scan-a", 50).GetAwaiter().GetResult(); + _fixture.SeedItemsAsync("scan-b", 50).GetAwaiter().GetResult(); } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + + // --- Scan (100 items, Limit=25, forces 4 pages) --- [Benchmark(Baseline = true), BenchmarkCategory("Scan")] public async Task AwsSdk_Scan() { var count = 0; - Dictionary? lastKey = null; + Dictionary? lastKey = null; do { var response = await _fixture.AwsSdkClient.ScanAsync(new ScanRequest { TableName = _fixture.TableName, - Limit = 20, + Limit = 25, ExclusiveStartKey = lastKey }); count += response.Items.Count; @@ -45,7 +54,7 @@ public async Task AwsSdk_Scan() } [Benchmark, BenchmarkCategory("Scan")] - public async Task Goa_Scan() + public async Task Goa_Scan_DynamoRecord() { var count = 0; Dictionary? lastKey = null; @@ -54,7 +63,7 @@ public async Task Goa_Scan() var response = await _fixture.GoaClient.ScanAsync(new GoaScanRequest { TableName = _fixture.TableName, - Limit = 20, + Limit = 25, ExclusiveStartKey = lastKey }); count += response.Value.Items.Count; @@ -73,7 +82,143 @@ public async Task Efficient_Scan() var response = await _fixture.EfficientClient.ScanAsync(new EfficientScanRequest { TableName = _fixture.TableName, - Limit = 20, + Limit = 25, + ExclusiveStartKey = lastKey + }); + count += response.Items.Count; + lastKey = response.LastEvaluatedKey; + } while (lastKey != null); + return count; + } + + // --- Scan With Filter (Limit=25, paginate through filtered results) --- + + [Benchmark(Baseline = true), BenchmarkCategory("Scan With Filter")] + public async Task AwsSdk_Scan_WithFilter() + { + var count = 0; + Dictionary? lastKey = null; + do + { + var response = await _fixture.AwsSdkClient.ScanAsync(new ScanRequest + { + TableName = _fixture.TableName, + FilterExpression = "#n > :minNum", + ExpressionAttributeNames = new Dictionary { ["#n"] = "number" }, + ExpressionAttributeValues = new Dictionary + { + [":minNum"] = new AwsAttributeValue { N = "25" } + }, + Limit = 25, + ExclusiveStartKey = lastKey + }); + count += response.Items.Count; + lastKey = response.LastEvaluatedKey?.Count > 0 ? response.LastEvaluatedKey : null; + } while (lastKey != null); + return count; + } + + [Benchmark, BenchmarkCategory("Scan With Filter")] + public async Task Goa_Scan_WithFilter_DynamoRecord() + { + var count = 0; + Dictionary? lastKey = null; + do + { + var response = await _fixture.GoaClient.ScanAsync(new GoaScanRequest + { + TableName = _fixture.TableName, + FilterExpression = "#n > :minNum", + ExpressionAttributeNames = new Dictionary { ["#n"] = "number" }, + ExpressionAttributeValues = new Dictionary + { + [":minNum"] = GoaModels.AttributeValue.Number("25") + }, + Limit = 25, + ExclusiveStartKey = lastKey + }); + count += response.Value.Items.Count; + lastKey = response.Value.HasMoreResults ? response.Value.LastEvaluatedKey : null; + } while (lastKey != null); + return count; + } + + [Benchmark, BenchmarkCategory("Scan With Filter")] + public async Task Efficient_Scan_WithFilter() + { + var count = 0; + IReadOnlyDictionary? lastKey = null; + do + { + var response = await _fixture.EfficientClient.ScanAsync(new EfficientScanRequest + { + TableName = _fixture.TableName, + FilterExpression = "#n > :minNum", + ExpressionAttributeNames = new Dictionary { ["#n"] = "number" }, + ExpressionAttributeValues = new Dictionary + { + [":minNum"] = new NumberAttributeValue("25") + }, + Limit = 25, + ExclusiveStartKey = lastKey + }); + count += response.Items.Count; + lastKey = response.LastEvaluatedKey; + } while (lastKey != null); + return count; + } + + // --- Scan With Limit (Limit=10, paginate through all results) --- + + [Benchmark(Baseline = true), BenchmarkCategory("Scan With Limit")] + public async Task AwsSdk_Scan_WithLimit() + { + var count = 0; + Dictionary? lastKey = null; + do + { + var response = await _fixture.AwsSdkClient.ScanAsync(new ScanRequest + { + TableName = _fixture.TableName, + Limit = 10, + ExclusiveStartKey = lastKey + }); + count += response.Items.Count; + lastKey = response.LastEvaluatedKey?.Count > 0 ? response.LastEvaluatedKey : null; + } while (lastKey != null); + return count; + } + + [Benchmark, BenchmarkCategory("Scan With Limit")] + public async Task Goa_Scan_WithLimit_DynamoRecord() + { + var count = 0; + Dictionary? lastKey = null; + do + { + var response = await _fixture.GoaClient.ScanAsync(new GoaScanRequest + { + TableName = _fixture.TableName, + Limit = 10, + ExclusiveStartKey = lastKey + }); + count += response.Value.Items.Count; + lastKey = response.Value.HasMoreResults ? response.Value.LastEvaluatedKey : null; + } while (lastKey != null); + return count; + } + + [Benchmark, BenchmarkCategory("Scan With Limit")] + public async Task Efficient_Scan_WithLimit() + { + var count = 0; + IReadOnlyDictionary? lastKey = null; + do + { + var response = await _fixture.EfficientClient.ScanAsync(new EfficientScanRequest + { + TableName = _fixture.TableName, + Limit = 10, ExclusiveStartKey = lastKey }); count += response.Items.Count; diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactGetItemsAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactGetItemsAsyncBenchmarks.cs index d0ada961..b06f9423 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactGetItemsAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactGetItemsAsyncBenchmarks.cs @@ -1,9 +1,12 @@ -using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; using EfficientDynamoDb.Operations.Shared; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; +using Goa.Clients.Dynamo.Operations.Transactions; using GoaModels = Goa.Clients.Dynamo.Models; +using EfficientTransactGetItemsRequest = EfficientDynamoDb.Operations.TransactGetItems.TransactGetItemsRequest; +using EfficientTransactGetItemsResponse = EfficientDynamoDb.Operations.TransactGetItems.TransactGetItemsResponse; +using EfficientTransactGetRequest = EfficientDynamoDb.Operations.TransactGetItems.TransactGetRequest; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -17,32 +20,103 @@ public void Setup() { _fixture = new LocalStackFixture(); _fixture.StartAsync().GetAwaiter().GetResult(); - _fixture.SeedItemsAsync("txget-bench", 10).GetAwaiter().GetResult(); + _fixture.SeedItemsAsync("transact-get", 10).GetAwaiter().GetResult(); } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true), BenchmarkCategory("Transact Get 5 Items")] + public async Task AwsSdk_TransactGet_5Items() + { + var items = new List(); + for (var i = 0; i < 5; i++) + { + items.Add(new Amazon.DynamoDBv2.Model.TransactGetItem + { + Get = new Get + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = new AttributeValue("transact-get"), + ["sk"] = new AttributeValue($"item-{i:D4}") + } + } + }); + } + return await _fixture.AwsSdkClient.TransactGetItemsAsync(new TransactGetItemsRequest + { + TransactItems = items + }); + } + + [Benchmark, BenchmarkCategory("Transact Get 5 Items")] + public async Task Goa_TransactGet_5Items() + { + var items = new List(); + for (var i = 0; i < 5; i++) + { + items.Add(new Goa.Clients.Dynamo.Operations.Transactions.TransactGetItem + { + Get = new TransactGetItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String("transact-get"), + ["sk"] = GoaModels.AttributeValue.String($"item-{i:D4}") + } + } + }); + } + var response = await _fixture.GoaClient.TransactGetItemsAsync(new TransactGetRequest + { + TransactItems = items + }); + return response.Value; + } + + [Benchmark, BenchmarkCategory("Transact Get 5 Items")] + public async Task Efficient_TransactGet_5Items() + { + var items = new List(); + for (var i = 0; i < 5; i++) + { + items.Add(new EfficientTransactGetRequest + { + TableName = _fixture.TableName, + Key = new PrimaryKey("pk", "transact-get", "sk", $"item-{i:D4}") + }); + } + return await _fixture.EfficientClient.TransactGetItemsAsync(new EfficientTransactGetItemsRequest + { + TransactItems = items + }); + } [Benchmark(Baseline = true), BenchmarkCategory("Transact Get 10 Items")] - public async Task AwsSdk_TransactGetItems() + public async Task AwsSdk_TransactGet_10Items() { - var items = new List(); + var items = new List(); for (var i = 0; i < 10; i++) { - items.Add(new TransactGetItem + items.Add(new Amazon.DynamoDBv2.Model.TransactGetItem { Get = new Get { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new("txget-bench"), - ["sk"] = new($"item-{i:D4}") + ["pk"] = new AttributeValue("transact-get"), + ["sk"] = new AttributeValue($"item-{i:D4}") } } }); } - return await _fixture.AwsSdkClient.TransactGetItemsAsync(new TransactGetItemsRequest { TransactItems = items @@ -50,26 +124,25 @@ public async Task AwsSdk_TransactGetItems() } [Benchmark, BenchmarkCategory("Transact Get 10 Items")] - public async Task Goa_TransactGetItems() + public async Task Goa_TransactGet_10Items() { var items = new List(); for (var i = 0; i < 10; i++) { items.Add(new Goa.Clients.Dynamo.Operations.Transactions.TransactGetItem { - Get = new Goa.Clients.Dynamo.Operations.Transactions.TransactGetItemRequest + Get = new TransactGetItemRequest { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new() { S = "txget-bench" }, - ["sk"] = new() { S = $"item-{i:D4}" } + ["pk"] = GoaModels.AttributeValue.String("transact-get"), + ["sk"] = GoaModels.AttributeValue.String($"item-{i:D4}") } } }); } - - var response = await _fixture.GoaClient.TransactGetItemsAsync(new Goa.Clients.Dynamo.Operations.Transactions.TransactGetRequest + var response = await _fixture.GoaClient.TransactGetItemsAsync(new TransactGetRequest { TransactItems = items }); @@ -77,19 +150,18 @@ public async Task AwsSdk_TransactGetItems() } [Benchmark, BenchmarkCategory("Transact Get 10 Items")] - public async Task Efficient_TransactGetItems() + public async Task Efficient_TransactGet_10Items() { - var items = new List(); + var items = new List(); for (var i = 0; i < 10; i++) { - items.Add(new EfficientDynamoDb.Operations.TransactGetItems.TransactGetRequest + items.Add(new EfficientTransactGetRequest { TableName = _fixture.TableName, - Key = new PrimaryKey("pk", "txget-bench", "sk", $"item-{i:D4}") + Key = new PrimaryKey("pk", "transact-get", "sk", $"item-{i:D4}") }); } - - return await _fixture.EfficientClient.TransactGetItemsAsync(new EfficientDynamoDb.Operations.TransactGetItems.TransactGetItemsRequest + return await _fixture.EfficientClient.TransactGetItemsAsync(new EfficientTransactGetItemsRequest { TransactItems = items }); diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactWriteItemsAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactWriteItemsAsyncBenchmarks.cs index 531ffca8..1673f229 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactWriteItemsAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/TransactWriteItemsAsyncBenchmarks.cs @@ -1,9 +1,18 @@ -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; using BenchmarkDotNet.Order; using EfficientDynamoDb.DocumentModel; +using EfficientDynamoDb.Operations.Shared; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; +using Goa.Clients.Dynamo.Operations.Transactions; using GoaModels = Goa.Clients.Dynamo.Models; +using AwsTransactWriteItem = Amazon.DynamoDBv2.Model.TransactWriteItem; +using EfficientTransactWriteItemsRequest = EfficientDynamoDb.Operations.TransactWriteItems.TransactWriteItemsRequest; +using EfficientTransactWriteItemsResponse = EfficientDynamoDb.Operations.TransactWriteItems.TransactWriteItemsResponse; +using EfficientAttributeValue = EfficientDynamoDb.DocumentModel.AttributeValue; +using AwsTransactWriteItemsRequest = Amazon.DynamoDBv2.Model.TransactWriteItemsRequest; +using AwsTransactWriteItemsResponse = Amazon.DynamoDBv2.Model.TransactWriteItemsResponse; +using AwsAttributeValue = Amazon.DynamoDBv2.Model.AttributeValue; +using AwsPut = Amazon.DynamoDBv2.Model.Put; +using AwsUpdate = Amazon.DynamoDBv2.Model.Update; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -21,88 +30,241 @@ public void Setup() } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } - [Benchmark(Baseline = true), BenchmarkCategory("Transact Write 10 Items")] - public async Task AwsSdk_TransactWriteItems() + [Benchmark(Baseline = true), BenchmarkCategory("Transact Write 5 Items")] + public async Task AwsSdk_TransactWrite_5Items() { - var baseCounter = Interlocked.Add(ref _counter, 10); - var items = new List(); - for (var i = 0; i < 10; i++) + var batch = Interlocked.Increment(ref _counter); + var items = new List(); + for (var i = 0; i < 5; i++) { - items.Add(new TransactWriteItem + items.Add(new AwsTransactWriteItem { - Put = new Put + Put = new AwsPut { TableName = _fixture.TableName, - Item = new Dictionary + Item = new Dictionary { - ["pk"] = new($"txw-aws-{baseCounter + i}"), - ["sk"] = new("item"), - ["data"] = new($"value-{baseCounter + i}") + ["pk"] = new AwsAttributeValue($"aws-tw-{batch}"), + ["sk"] = new AwsAttributeValue($"item-{i:D4}"), + ["data"] = new AwsAttributeValue($"value-{i}") } } }); } - - return await _fixture.AwsSdkClient.TransactWriteItemsAsync(new TransactWriteItemsRequest + return await _fixture.AwsSdkClient.TransactWriteItemsAsync(new AwsTransactWriteItemsRequest { TransactItems = items }); } - [Benchmark, BenchmarkCategory("Transact Write 10 Items")] - public async Task Goa_TransactWriteItems() + [Benchmark, BenchmarkCategory("Transact Write 5 Items")] + public async Task Goa_TransactWrite_5Items() { - var baseCounter = Interlocked.Add(ref _counter, 10); + var batch = Interlocked.Increment(ref _counter); var items = new List(); - for (var i = 0; i < 10; i++) + for (var i = 0; i < 5; i++) { items.Add(new Goa.Clients.Dynamo.Operations.Transactions.TransactWriteItem { - Put = new Goa.Clients.Dynamo.Operations.Transactions.TransactPutItem + Put = new TransactPutItem { TableName = _fixture.TableName, Item = new Dictionary { - ["pk"] = new() { S = $"txw-goa-{baseCounter + i}" }, - ["sk"] = new() { S = "item" }, - ["data"] = new() { S = $"value-{baseCounter + i}" } + ["pk"] = GoaModels.AttributeValue.String($"goa-tw-{batch}"), + ["sk"] = GoaModels.AttributeValue.String($"item-{i:D4}"), + ["data"] = GoaModels.AttributeValue.String($"value-{i}") } } }); } - - var response = await _fixture.GoaClient.TransactWriteItemsAsync(new Goa.Clients.Dynamo.Operations.Transactions.TransactWriteRequest + var response = await _fixture.GoaClient.TransactWriteItemsAsync(new TransactWriteRequest { TransactItems = items }); - return response.Value; + return !response.IsError; } - [Benchmark, BenchmarkCategory("Transact Write 10 Items")] - public async Task Efficient_TransactWriteItems() + [Benchmark, BenchmarkCategory("Transact Write 5 Items")] + public async Task Efficient_TransactWrite_5Items() { - var baseCounter = Interlocked.Add(ref _counter, 10); + var batch = Interlocked.Increment(ref _counter); var items = new List(); - for (var i = 0; i < 10; i++) + for (var i = 0; i < 5; i++) { - items.Add(new EfficientDynamoDb.Operations.TransactWriteItems.TransactWriteItem( - new EfficientDynamoDb.Operations.TransactWriteItems.TransactPutItem + items.Add(new EfficientDynamoDb.Operations.TransactWriteItems.TransactWriteItem(new EfficientDynamoDb.Operations.TransactWriteItems.TransactPutItem + { + TableName = _fixture.TableName, + Item = new Document { - TableName = _fixture.TableName, - Item = new Document + ["pk"] = $"eff-tw-{batch}", + ["sk"] = $"item-{i:D4}", + ["data"] = $"value-{i}" + } + })); + } + return await _fixture.EfficientClient.TransactWriteItemsAsync(new EfficientTransactWriteItemsRequest + { + TransactItems = items + }); + } + + [Benchmark(Baseline = true), BenchmarkCategory("Transact Write Mixed Ops")] + public async Task AwsSdk_TransactWrite_MixedOps() + { + var batch = Interlocked.Increment(ref _counter); + // First seed an item to update/delete + await _fixture.AwsSdkClient.PutItemAsync(new Amazon.DynamoDBv2.Model.PutItemRequest + { + TableName = _fixture.TableName, + Item = new Dictionary + { + ["pk"] = new AwsAttributeValue($"aws-twm-{batch}"), + ["sk"] = new AwsAttributeValue("existing"), + ["data"] = new AwsAttributeValue("seed") + } + }); + + return await _fixture.AwsSdkClient.TransactWriteItemsAsync(new AwsTransactWriteItemsRequest + { + TransactItems = + [ + new AwsTransactWriteItem + { + Put = new AwsPut { - ["pk"] = $"txw-eff-{baseCounter + i}", - ["sk"] = "item", - ["data"] = $"value-{baseCounter + i}" + TableName = _fixture.TableName, + Item = new Dictionary + { + ["pk"] = new AwsAttributeValue($"aws-twm-{batch}"), + ["sk"] = new AwsAttributeValue("new-item"), + ["data"] = new AwsAttributeValue("created") + } } - })); - } + }, + new AwsTransactWriteItem + { + Update = new AwsUpdate + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = new AwsAttributeValue($"aws-twm-{batch}"), + ["sk"] = new AwsAttributeValue("existing") + }, + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = new AwsAttributeValue("updated") + } + } + } + ] + }); + } - return await _fixture.EfficientClient.TransactWriteItemsAsync(new EfficientDynamoDb.Operations.TransactWriteItems.TransactWriteItemsRequest + [Benchmark, BenchmarkCategory("Transact Write Mixed Ops")] + public async Task Goa_TransactWrite_MixedOps() + { + var batch = Interlocked.Increment(ref _counter); + await _fixture.GoaClient.PutItemAsync(new Goa.Clients.Dynamo.Operations.PutItem.PutItemRequest { - TransactItems = items + TableName = _fixture.TableName, + Item = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String($"goa-twm-{batch}"), + ["sk"] = GoaModels.AttributeValue.String("existing"), + ["data"] = GoaModels.AttributeValue.String("seed") + } + }); + + var response = await _fixture.GoaClient.TransactWriteItemsAsync(new TransactWriteRequest + { + TransactItems = + [ + new Goa.Clients.Dynamo.Operations.Transactions.TransactWriteItem + { + Put = new TransactPutItem + { + TableName = _fixture.TableName, + Item = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String($"goa-twm-{batch}"), + ["sk"] = GoaModels.AttributeValue.String("new-item"), + ["data"] = GoaModels.AttributeValue.String("created") + } + } + }, + new Goa.Clients.Dynamo.Operations.Transactions.TransactWriteItem + { + Update = new TransactUpdateItem + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String($"goa-twm-{batch}"), + ["sk"] = GoaModels.AttributeValue.String("existing") + }, + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = GoaModels.AttributeValue.String("updated") + } + } + } + ] + }); + return !response.IsError; + } + + [Benchmark, BenchmarkCategory("Transact Write Mixed Ops")] + public async Task Efficient_TransactWrite_MixedOps() + { + var batch = Interlocked.Increment(ref _counter); + await _fixture.EfficientClient.PutItemAsync(new EfficientDynamoDb.Operations.PutItem.PutItemRequest + { + TableName = _fixture.TableName, + Item = new Document + { + ["pk"] = $"eff-twm-{batch}", + ["sk"] = "existing", + ["data"] = "seed" + } + }); + + return await _fixture.EfficientClient.TransactWriteItemsAsync(new EfficientTransactWriteItemsRequest + { + TransactItems = + [ + new EfficientDynamoDb.Operations.TransactWriteItems.TransactWriteItem(new EfficientDynamoDb.Operations.TransactWriteItems.TransactPutItem + { + TableName = _fixture.TableName, + Item = new Document + { + ["pk"] = $"eff-twm-{batch}", + ["sk"] = "new-item", + ["data"] = "created" + } + }), + new EfficientDynamoDb.Operations.TransactWriteItems.TransactWriteItem(new EfficientDynamoDb.Operations.TransactWriteItems.TransactUpdateItem + { + TableName = _fixture.TableName, + Key = new PrimaryKey("pk", $"eff-twm-{batch}", "sk", "existing"), + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = "updated" + } + }) + ] }); } } diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/UpdateItemAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/UpdateItemAsyncBenchmarks.cs index 9c4de7ad..e56a97c7 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/UpdateItemAsyncBenchmarks.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/UpdateItemAsyncBenchmarks.cs @@ -4,8 +4,9 @@ using EfficientDynamoDb.Operations.Shared; using Goa.Clients.Dynamo.Benchmarks.Infrastructure; using GoaModels = Goa.Clients.Dynamo.Models; -using EfficientUpdateItemResponse = EfficientDynamoDb.Operations.UpdateItem.UpdateItemResponse; using EfficientAttributeValue = EfficientDynamoDb.DocumentModel.AttributeValue; +using EfficientUpdateItemRequest = EfficientDynamoDb.Operations.UpdateItem.UpdateItemRequest; +using EfficientUpdateItemResponse = EfficientDynamoDb.Operations.UpdateItem.UpdateItemResponse; namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; @@ -13,76 +14,224 @@ namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks; public class UpdateItemAsyncBenchmarks { private LocalStackFixture _fixture = null!; - private int _counter; [GlobalSetup] public void Setup() { _fixture = new LocalStackFixture(); _fixture.StartAsync().GetAwaiter().GetResult(); - _fixture.SeedItemsAsync("update-bench", 1).GetAwaiter().GetResult(); + + // Seed items for update benchmarks + _fixture.SeedItemsAsync("update-bench", 10).GetAwaiter().GetResult(); } [GlobalCleanup] - public void Cleanup() => _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + public void Cleanup() + { + _fixture.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } - [Benchmark(Baseline = true), BenchmarkCategory("Update Item")] - public async Task AwsSdk_UpdateItem() + [Benchmark(Baseline = true), BenchmarkCategory("Update Single Attribute")] + public async Task AwsSdk_UpdateItem_SingleAttribute() { - var i = Interlocked.Increment(ref _counter); return await _fixture.AwsSdkClient.UpdateItemAsync(new UpdateItemRequest { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new("update-bench"), - ["sk"] = new("item-0000") + ["pk"] = new AttributeValue("update-bench"), + ["sk"] = new AttributeValue("item-0000") + }, + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary + { + ["#d"] = "data" }, - UpdateExpression = "SET #d = :d", - ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, ExpressionAttributeValues = new Dictionary { - [":d"] = new($"updated-{i}") + [":val"] = new AttributeValue("updated-value") } }); } - [Benchmark, BenchmarkCategory("Update Item")] - public async Task Goa_UpdateItem() + [Benchmark, BenchmarkCategory("Update Single Attribute")] + public async Task Goa_UpdateItem_SingleAttribute() { - var i = Interlocked.Increment(ref _counter); var response = await _fixture.GoaClient.UpdateItemAsync(new Goa.Clients.Dynamo.Operations.UpdateItem.UpdateItemRequest { TableName = _fixture.TableName, Key = new Dictionary { - ["pk"] = new() { S = "update-bench" }, - ["sk"] = new() { S = "item-0000" } + ["pk"] = GoaModels.AttributeValue.String("update-bench"), + ["sk"] = GoaModels.AttributeValue.String("item-0000") + }, + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary + { + ["#d"] = "data" }, - UpdateExpression = "SET #d = :d", - ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, ExpressionAttributeValues = new Dictionary { - [":d"] = new() { S = $"updated-{i}" } + [":val"] = GoaModels.AttributeValue.String("updated-value") } }); - return response.Value; + return !response.IsError; } - [Benchmark, BenchmarkCategory("Update Item")] - public async Task Efficient_UpdateItem() + [Benchmark, BenchmarkCategory("Update Single Attribute")] + public async Task Efficient_UpdateItem_SingleAttribute() { - var i = Interlocked.Increment(ref _counter); - return await _fixture.EfficientClient.UpdateItemAsync(new EfficientDynamoDb.Operations.UpdateItem.UpdateItemRequest + return await _fixture.EfficientClient.UpdateItemAsync(new EfficientUpdateItemRequest { TableName = _fixture.TableName, Key = new PrimaryKey("pk", "update-bench", "sk", "item-0000"), - UpdateExpression = "SET #d = :d", - ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary + { + ["#d"] = "data" + }, ExpressionAttributeValues = new Dictionary { - [":d"] = $"updated-{i}" + [":val"] = "updated-value" } }); } + + [Benchmark(Baseline = true), BenchmarkCategory("Update Multiple Attributes")] + public async Task AwsSdk_UpdateItem_MultipleAttributes() + { + return await _fixture.AwsSdkClient.UpdateItemAsync(new UpdateItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = new AttributeValue("update-bench"), + ["sk"] = new AttributeValue("item-0001") + }, + UpdateExpression = "SET #d = :val, #s = :status, #n = #n + :inc", + ExpressionAttributeNames = new Dictionary + { + ["#d"] = "data", + ["#s"] = "status", + ["#n"] = "number" + }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = new AttributeValue("multi-updated"), + [":status"] = new AttributeValue("modified"), + [":inc"] = new AttributeValue { N = "1" } + } + }); + } + + [Benchmark, BenchmarkCategory("Update Multiple Attributes")] + public async Task Goa_UpdateItem_MultipleAttributes() + { + var response = await _fixture.GoaClient.UpdateItemAsync(new Goa.Clients.Dynamo.Operations.UpdateItem.UpdateItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String("update-bench"), + ["sk"] = GoaModels.AttributeValue.String("item-0001") + }, + UpdateExpression = "SET #d = :val, #s = :status, #n = #n + :inc", + ExpressionAttributeNames = new Dictionary + { + ["#d"] = "data", + ["#s"] = "status", + ["#n"] = "number" + }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = GoaModels.AttributeValue.String("multi-updated"), + [":status"] = GoaModels.AttributeValue.String("modified"), + [":inc"] = GoaModels.AttributeValue.Number("1") + } + }); + return !response.IsError; + } + + [Benchmark, BenchmarkCategory("Update Multiple Attributes")] + public async Task Efficient_UpdateItem_MultipleAttributes() + { + return await _fixture.EfficientClient.UpdateItemAsync(new EfficientUpdateItemRequest + { + TableName = _fixture.TableName, + Key = new PrimaryKey("pk", "update-bench", "sk", "item-0001"), + UpdateExpression = "SET #d = :val, #s = :status, #n = #n + :inc", + ExpressionAttributeNames = new Dictionary + { + ["#d"] = "data", + ["#s"] = "status", + ["#n"] = "number" + }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = "multi-updated", + [":status"] = "modified", + [":inc"] = new EfficientDynamoDb.DocumentModel.NumberAttributeValue("1") + } + }); + } + + [Benchmark(Baseline = true), BenchmarkCategory("Update With Return Values")] + public async Task AwsSdk_UpdateItem_WithReturnValues() + { + return await _fixture.AwsSdkClient.UpdateItemAsync(new UpdateItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = new AttributeValue("update-bench"), + ["sk"] = new AttributeValue("item-0002") + }, + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = new AttributeValue("return-val") + }, + ReturnValues = ReturnValue.ALL_NEW + }); + } + + [Benchmark, BenchmarkCategory("Update With Return Values")] + public async Task Goa_UpdateItem_WithReturnValues() + { + var response = await _fixture.GoaClient.UpdateItemAsync(new Goa.Clients.Dynamo.Operations.UpdateItem.UpdateItemRequest + { + TableName = _fixture.TableName, + Key = new Dictionary + { + ["pk"] = GoaModels.AttributeValue.String("update-bench"), + ["sk"] = GoaModels.AttributeValue.String("item-0002") + }, + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = GoaModels.AttributeValue.String("return-val") + }, + ReturnValues = Goa.Clients.Dynamo.Enums.ReturnValues.ALL_NEW + }); + return response.Value.Attributes; + } + + [Benchmark, BenchmarkCategory("Update With Return Values")] + public async Task Efficient_UpdateItem_WithReturnValues() + { + return await _fixture.EfficientClient.UpdateItemAsync(new EfficientUpdateItemRequest + { + TableName = _fixture.TableName, + Key = new PrimaryKey("pk", "update-bench", "sk", "item-0002"), + UpdateExpression = "SET #d = :val", + ExpressionAttributeNames = new Dictionary { ["#d"] = "data" }, + ExpressionAttributeValues = new Dictionary + { + [":val"] = "return-val" + }, + ReturnValues = EfficientDynamoDb.Operations.Shared.ReturnValues.AllNew + }); + } } diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/CodeGeneration/MapperGeneratorTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/CodeGeneration/MapperGeneratorTests.cs index 8cfa683b..01f08637 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/CodeGeneration/MapperGeneratorTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/CodeGeneration/MapperGeneratorTests.cs @@ -103,9 +103,9 @@ public async Task GenerateCode_WithPrimaryKeys_ShouldGenerateKeyAssignments() // Assert await Assert.That(result) - .Contains("record[\"PK\"] = new AttributeValue { S = $\"USER#{ model.Id?.ToString() ?? \"\" }\" };"); + .Contains("record[\"PK\"] = AttributeValue.String($\"USER#{ model.Id?.ToString() ?? \"\" }\");"); await Assert.That(result) - .Contains("record[\"SK\"] = new AttributeValue { S = \"METADATA\" };"); + .Contains("record[\"SK\"] = AttributeValue.String(\"METADATA\");"); } [Test] @@ -140,9 +140,9 @@ public async Task GenerateCode_WithCustomPKSKNames_ShouldUseCustomNames() // Assert await Assert.That(result) - .Contains("record[\"HashKey\"] = new AttributeValue { S = $\"USER#{ model.Id?.ToString() ?? \"\" }\" };"); + .Contains("record[\"HashKey\"] = AttributeValue.String($\"USER#{ model.Id?.ToString() ?? \"\" }\");"); await Assert.That(result) - .Contains("record[\"RangeKey\"] = new AttributeValue { S = \"METADATA\" };"); + .Contains("record[\"RangeKey\"] = AttributeValue.String(\"METADATA\");"); } [Test] @@ -184,9 +184,9 @@ public async Task GenerateCode_WithGSI_ShouldGenerateGSIAssignments() // Assert - Based on actual generated output await Assert.That(result) - .Contains("record[\"GSI1PK\"] = new AttributeValue { S = $\"EMAIL#{ model.Email?.ToString() ?? \"\" }\" };"); + .Contains("record[\"GSI1PK\"] = AttributeValue.String($\"EMAIL#{ model.Email?.ToString() ?? \"\" }\");"); await Assert.That(result) - .Contains("record[\"GSI1SK\"] = new AttributeValue { S = $\"USER#{ model.Id?.ToString() ?? \"\" }\" };"); + .Contains("record[\"GSI1SK\"] = AttributeValue.String($\"USER#{ model.Id?.ToString() ?? \"\" }\");"); } [Test] @@ -219,13 +219,17 @@ public async Task GenerateCode_WithPropertyMappings_ShouldGeneratePropertyCode() // Act var result = _generator.GenerateCode(types, context); - // Assert - Based on actual output from debug test + // Assert - Strings use conditional assignment to skip empty strings, Age uses direct assignment + await Assert.That(result) + .Contains("if (!string.IsNullOrEmpty(model.Id))"); + await Assert.That(result) + .Contains("record[\"Id\"] = AttributeValue.String(model.Id);"); await Assert.That(result) - .Contains("record[\"Id\"] = new AttributeValue { S = model.Id };"); + .Contains("if (!string.IsNullOrEmpty(model.Name))"); await Assert.That(result) - .Contains("record[\"Name\"] = new AttributeValue { S = model.Name };"); + .Contains("record[\"Name\"] = AttributeValue.String(model.Name);"); await Assert.That(result) - .Contains("record[\"Age\"] = new AttributeValue { N = model.Age.ToString(CultureInfo.InvariantCulture) };"); + .Contains("record[\"Age\"] = AttributeValue.Number(model.Age.ToString(CultureInfo.InvariantCulture));"); } [Test] @@ -260,11 +264,11 @@ public async Task GenerateCode_WithComplexProperties_ShouldUseTypeHandlers() // Assert // DateTime should use DateTimeTypeHandler await Assert.That(result) - .Contains("record[\"CreatedAt\"] = new AttributeValue { S = model.CreatedAt.ToString(\"o\") };"); + .Contains("record[\"CreatedAt\"] = AttributeValue.String(model.CreatedAt.ToString(\"o\"));"); // UnixTimestamp should use UnixTimestampTypeHandler await Assert.That(result) - .Contains("record[\"UpdatedAt\"] = new AttributeValue { N = ((DateTimeOffset)model.UpdatedAt).ToUnixTimeSeconds().ToString() };"); + .Contains("record[\"UpdatedAt\"] = AttributeValue.Number(((DateTimeOffset)model.UpdatedAt).ToUnixTimeSeconds().ToString());"); } [Test] @@ -303,7 +307,7 @@ public async Task GenerateCode_WithCollectionProperties_ShouldUseCollectionHandl // Assert - Based on actual CollectionTypeHandler output await Assert.That(result) - .Contains("record[\"Tags\"] = (model.Tags != null && model.Tags.Any() ? new AttributeValue { SS = model.Tags.ToList() } : new AttributeValue { NULL = true });"); + .Contains("record[\"Tags\"] = (model.Tags != null && model.Tags.Any() ? AttributeValue.FromStringSet(model.Tags.ToList()) : AttributeValue.Null());"); } [Test] diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/Integration/NullAwarenessIntegrationTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/Integration/NullAwarenessIntegrationTests.cs index 3f6f5fbe..4eab0874 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/Integration/NullAwarenessIntegrationTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/Integration/NullAwarenessIntegrationTests.cs @@ -72,7 +72,7 @@ public async Task NonNullableIntProperty_ShouldGenerateCorrectToAttributeValueCo var result = _typeHandlerRegistry.GenerateToAttributeValue(property); - var expected = "new AttributeValue { N = model.Id.ToString(CultureInfo.InvariantCulture) }"; + var expected = "AttributeValue.Number(model.Id.ToString(CultureInfo.InvariantCulture))"; await Assert.That(result).IsEqualTo(expected); } @@ -144,13 +144,13 @@ public async Task CompleteModel_ShouldGenerateCorrectMapperCode() // Verify the generated code contains correct null handling // Non-nullable strings now use conditional assignment to skip empty strings await Assert.That(generatedCode).Contains("if (!string.IsNullOrEmpty(model.RequiredName))"); - await Assert.That(generatedCode).Contains("record[\"RequiredName\"] = new AttributeValue { S = model.RequiredName };"); + await Assert.That(generatedCode).Contains("record[\"RequiredName\"] = AttributeValue.String(model.RequiredName);"); // Nullable strings also use conditional assignment with IsNullOrEmpty check await Assert.That(generatedCode).Contains("if (!string.IsNullOrEmpty(model.OptionalDescription))"); - await Assert.That(generatedCode).Contains("record[\"OptionalDescription\"] = new AttributeValue { S = model.OptionalDescription };"); + await Assert.That(generatedCode).Contains("record[\"OptionalDescription\"] = AttributeValue.String(model.OptionalDescription);"); // Nullable properties use conditional assignment for sparse GSI compatibility await Assert.That(generatedCode).Contains("if (model.OptionalCount.HasValue)"); - await Assert.That(generatedCode).Contains("new AttributeValue { N = model.OptionalCount.Value.ToString(CultureInfo.InvariantCulture) }"); + await Assert.That(generatedCode).Contains("AttributeValue.Number(model.OptionalCount.Value.ToString(CultureInfo.InvariantCulture))"); // Verify MissingAttributeException for non-nullable types await Assert.That(generatedCode).Contains("MissingAttributeException.Throw(\"RequiredName\""); @@ -181,7 +181,7 @@ public async Task DateOnlyProperties_ShouldHandleNullabilityCorrectly() await Assert.That(nullableResult).IsNull(); await Assert.That(nonNullableResult).DoesNotContain("HasValue"); - await Assert.That(nonNullableResult).DoesNotContain("NULL = true"); + await Assert.That(nonNullableResult).DoesNotContain("AttributeValue.Null()"); } [Test] @@ -446,7 +446,7 @@ await Assert.That(toAttributeValue) .Because("Should check each item for null before converting"); await Assert.That(toAttributeValue) - .Contains("NULL = true") + .Contains("AttributeValue.Null()") .Because("Null items should be preserved as DynamoDB NULL attributes"); await Assert.That(toAttributeValue) diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/CollectionTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/CollectionTypeHandlerTests.cs index 1eb04323..de2cdc09 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/CollectionTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/CollectionTypeHandlerTests.cs @@ -118,7 +118,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateStringSet_ForStringColl var property = TestModelBuilders.CreateCollectionPropertyInfo(propName, collectionType, MockSymbolFactory.PrimitiveTypes.String); var result = _handler.GenerateToAttributeValue(property); - var expected = $"(model.{propName} != null && model.{propName}.Any() ? new AttributeValue {{ SS = model.{propName}.ToList() }} : new AttributeValue {{ NULL = true }})"; + var expected = $"(model.{propName} != null && model.{propName}.Any() ? AttributeValue.FromStringSet(model.{propName}.ToList()) : AttributeValue.Null())"; await Assert.That(result) .IsEqualTo(expected) .Because($"String collections should generate SS (String Set) attribute value only when non-empty"); @@ -140,7 +140,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateNumberSet_ForNumericCol var property = TestModelBuilders.CreateCollectionPropertyInfo(propName, collectionType, elementType); var result = _handler.GenerateToAttributeValue(property); - var expected = $"(model.{propName} != null && model.{propName}.Any() ? new AttributeValue {{ NS = model.{propName}.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList() }} : new AttributeValue {{ NULL = true }})"; + var expected = $"(model.{propName} != null && model.{propName}.Any() ? AttributeValue.FromNumberSet(model.{propName}.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList()) : AttributeValue.Null())"; await Assert.That(result) .IsEqualTo(expected) .Because($"Numeric collections should generate NS (Number Set) attribute value only when non-empty, with invariant culture"); @@ -155,7 +155,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateStringSet_ForBooleanCol var result = _handler.GenerateToAttributeValue(property); - var expected = "(model.BoolArray != null && model.BoolArray.Any() ? new AttributeValue { SS = model.BoolArray.Select(x => x.ToString()).ToList() } : new AttributeValue { NULL = true })"; + var expected = "(model.BoolArray != null && model.BoolArray.Any() ? AttributeValue.FromStringSet(model.BoolArray.Select(x => x.ToString()).ToList()) : AttributeValue.Null())"; await Assert.That(result) .IsEqualTo(expected) .Because("Boolean collections should generate SS with ToString() conversion only when non-empty"); @@ -169,7 +169,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateStringSet_ForGuidCollec var result = _handler.GenerateToAttributeValue(property); - var expected = "(model.GuidArray != null && model.GuidArray.Any() ? new AttributeValue { SS = model.GuidArray.Select(x => x.ToString()).ToList() } : new AttributeValue { NULL = true })"; + var expected = "(model.GuidArray != null && model.GuidArray.Any() ? AttributeValue.FromStringSet(model.GuidArray.Select(x => x.ToString()).ToList()) : AttributeValue.Null())"; await Assert.That(result) .IsEqualTo(expected) .Because("Guid collections should generate SS with ToString() conversion only when non-empty"); @@ -183,7 +183,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateStringSet_ForDateTimeCo var result = _handler.GenerateToAttributeValue(property); - var expected = "(model.DateTimeArray != null && model.DateTimeArray.Any() ? new AttributeValue { SS = model.DateTimeArray.Select(x => x.ToString(\"o\")).ToList() } : new AttributeValue { NULL = true })"; + var expected = "(model.DateTimeArray != null && model.DateTimeArray.Any() ? AttributeValue.FromStringSet(model.DateTimeArray.Select(x => x.ToString(\"o\")).ToList()) : AttributeValue.Null())"; await Assert.That(result) .IsEqualTo(expected) .Because("DateTime collections should generate SS with ISO format only when non-empty"); @@ -202,7 +202,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateStringSet_ForEnumCollec var result = _handler.GenerateToAttributeValue(property); - var expected = "(model.PriorityArray != null && model.PriorityArray.Any() ? new AttributeValue { SS = model.PriorityArray.Select(x => x.ToString()).ToList() } : new AttributeValue { NULL = true })"; + var expected = "(model.PriorityArray != null && model.PriorityArray.Any() ? AttributeValue.FromStringSet(model.PriorityArray.Select(x => x.ToString()).ToList()) : AttributeValue.Null())"; await Assert.That(result) .IsEqualTo(expected) .Because("Enum collections should generate SS with ToString() conversion only when non-empty"); @@ -383,7 +383,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateMapList_ForComplexColle var result = _handler.GenerateToAttributeValue(property); - var expected = "model.CustomList != null ? new AttributeValue { L = model.CustomList.Select(item => (item != null ? new AttributeValue { M = DynamoMapper.CustomClass.ToDynamoRecord(item) } : new AttributeValue { NULL = true })).ToList() } : new AttributeValue { NULL = true }"; + var expected = "model.CustomList != null ? AttributeValue.FromList(model.CustomList.Select(item => (item != null ? AttributeValue.FromMap(DynamoMapper.CustomClass.ToDynamoRecord(item)) : AttributeValue.Null())).ToList()) : AttributeValue.Null()"; await Assert.That(result) .IsEqualTo(expected) .Because("Complex type collections should generate L (List) attribute with M (Map) elements and handle null items"); @@ -446,7 +446,7 @@ await Assert.That(result) .Because("Complex type collections should check for null items before converting"); await Assert.That(result) - .Contains("new AttributeValue { NULL = true }") + .Contains("AttributeValue.Null()") .Because("Null items should be represented as DynamoDB NULL attribute"); } @@ -471,8 +471,8 @@ public async Task GenerateToAttributeValue_NestedStringCollection_TwoLevels_Shou // Assert await Assert.That(result) - .Contains("L =") - .Because("Nested collections should use L (List) attribute type"); + .Contains("AttributeValue.FromList(") + .Because("Nested collections should use List attribute type"); await Assert.That(result) .IsNotNull() @@ -500,8 +500,8 @@ public async Task GenerateToAttributeValue_NestedIntCollection_TwoLevels_ShouldG // Assert await Assert.That(result) - .Contains("L =") - .Because("Nested numeric collections should use L (List) attribute type"); + .Contains("AttributeValue.FromList(") + .Because("Nested numeric collections should use List attribute type"); await Assert.That(result) .IsNotNull() @@ -533,7 +533,7 @@ await Assert.That(result) .Because("Nested collections should check for null inner collections"); await Assert.That(result) - .Contains("NULL = true") + .Contains("AttributeValue.Null()") .Because("Null inner collections should be represented as DynamoDB NULL"); } @@ -639,7 +639,7 @@ await Assert.That(result) .Because("Three-level nested collections should be supported"); await Assert.That(result) - .Contains("L =") + .Contains("AttributeValue.FromList(") .Because("Deep nested collections should use List attribute type"); } @@ -656,11 +656,11 @@ public async Task GenerateToAttributeValue_CollectionOfNullableElements_ShouldNo // Assert await Assert.That(result) - .Contains("item != null ? new AttributeValue { M =") + .Contains("item != null ? AttributeValue.FromMap(") .Because("Should check each item for null"); await Assert.That(result) - .Contains(": new AttributeValue { NULL = true }") + .Contains(": AttributeValue.Null()") .Because("Null items should be preserved as NULL attributes, not filtered out"); } @@ -685,7 +685,7 @@ public async Task GenerateToAttributeValue_NestedStringCollection_ShouldUseCorre // Assert - The inner primitive collection mapping should use "item" (the outer lambda var), // not a hard-coded identifier that doesn't match the lambda parameter - var expected = "model.Tags != null ? new AttributeValue { L = model.Tags.Select(item => (item != null && item.Any() ? new AttributeValue { SS = item.ToList() } : new AttributeValue { NULL = true })).ToList() } : new AttributeValue { NULL = true }"; + var expected = "model.Tags != null ? AttributeValue.FromList(model.Tags.Select(item => (item != null && item.Any() ? AttributeValue.FromStringSet(item.ToList()) : AttributeValue.Null())).ToList()) : AttributeValue.Null()"; await Assert.That(result) .IsEqualTo(expected) .Because("Nested string collection should use the lambda variable 'item' in the inner SS mapping"); @@ -712,7 +712,7 @@ public async Task GenerateToAttributeValue_NestedIntCollection_ShouldUseCorrectI // Assert - The inner primitive collection mapping should use "item" (the outer lambda var), // not a hard-coded identifier that doesn't match the lambda parameter - var expected = "model.Scores != null ? new AttributeValue { L = model.Scores.Select(item => (item != null && item.Any() ? new AttributeValue { NS = item.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList() } : new AttributeValue { NULL = true })).ToList() } : new AttributeValue { NULL = true }"; + var expected = "model.Scores != null ? AttributeValue.FromList(model.Scores.Select(item => (item != null && item.Any() ? AttributeValue.FromNumberSet(item.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList()) : AttributeValue.Null())).ToList()) : AttributeValue.Null()"; await Assert.That(result) .IsEqualTo(expected) .Because("Nested int collection should use the lambda variable 'item' in the inner NS mapping"); diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/ComplexTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/ComplexTypeHandlerTests.cs index 3c276a3d..b411d16d 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/ComplexTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/ComplexTypeHandlerTests.cs @@ -209,7 +209,7 @@ public async Task GenerateToAttributeValue_ComplexType_ShouldGenerateCorrectCode var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "model.HomeAddress != null ? new AttributeValue { M = DynamoMapper.Address.ToDynamoRecord(model.HomeAddress) } : new AttributeValue { NULL = true }"; + var expected = "model.HomeAddress != null ? AttributeValue.FromMap(DynamoMapper.Address.ToDynamoRecord(model.HomeAddress)) : AttributeValue.Null()"; await Assert.That(result) .IsEqualTo(expected); } @@ -236,7 +236,7 @@ public async Task GenerateToAttributeValue_StringStringDictionary_ShouldGenerate var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "model.Metadata != null ? new AttributeValue { M = model.Metadata.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue { S = kvp.Value }) } : new AttributeValue { NULL = true }"; + var expected = "model.Metadata != null ? AttributeValue.FromMap(model.Metadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value != null ? AttributeValue.String(kvp.Value) : AttributeValue.Null())) : AttributeValue.Null()"; await Assert.That(result) .IsEqualTo(expected); } @@ -263,7 +263,7 @@ public async Task GenerateToAttributeValue_StringIntDictionary_ShouldGenerateCor var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "model.Scores != null ? new AttributeValue { M = model.Scores.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue { N = kvp.Value.ToString(CultureInfo.InvariantCulture) }) } : new AttributeValue { NULL = true }"; + var expected = "model.Scores != null ? AttributeValue.FromMap(model.Scores.ToDictionary(kvp => kvp.Key, kvp => AttributeValue.Number(kvp.Value.ToString(CultureInfo.InvariantCulture)))) : AttributeValue.Null()"; await Assert.That(result) .IsEqualTo(expected); } @@ -296,7 +296,7 @@ public async Task GenerateToAttributeValue_StringListDictionary_ShouldGenerateCo var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "model.TagsByCategory != null ? new AttributeValue { M = model.TagsByCategory.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null || kvp.Value.Count == 0 ? new AttributeValue { NULL = true } : new AttributeValue { SS = kvp.Value }) } : new AttributeValue { NULL = true }"; + var expected = "model.TagsByCategory != null ? AttributeValue.FromMap(model.TagsByCategory.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null || kvp.Value.Count == 0 ? AttributeValue.Null() : AttributeValue.FromStringSet(kvp.Value))) : AttributeValue.Null()"; await Assert.That(result) .IsEqualTo(expected); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateOnlyTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateOnlyTypeHandlerTests.cs index 5c586fc9..fc620c1a 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateOnlyTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateOnlyTypeHandlerTests.cs @@ -61,7 +61,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateCorrectCode_ForNonNulla var result = _handler.GenerateToAttributeValue(property); - var expected = "new AttributeValue { S = model.CreatedDate.ToString(\"yyyy-MM-dd\") }"; + var expected = "AttributeValue.String(model.CreatedDate.ToString(\"yyyy-MM-dd\"))"; await Assert.That(result).IsEqualTo(expected); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateTimeTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateTimeTypeHandlerTests.cs index 2fe97733..3fd94b43 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateTimeTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/DateTimeTypeHandlerTests.cs @@ -123,7 +123,7 @@ public async Task GenerateToAttributeValue_NonNullableDateTime_ShouldGenerateCor var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "new AttributeValue { S = model.CreatedAt.ToString(\"o\") }"; + var expected = "AttributeValue.String(model.CreatedAt.ToString(\"o\"))"; await Assert.That(result) .IsEqualTo(expected); } @@ -161,7 +161,7 @@ public async Task GenerateToAttributeValue_NonNullableDateTimeOffset_ShouldGener var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "new AttributeValue { S = model.CreatedAt.ToString(\"o\") }"; + var expected = "AttributeValue.String(model.CreatedAt.ToString(\"o\"))"; await Assert.That(result) .IsEqualTo(expected); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/EnumTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/EnumTypeHandlerTests.cs index faa00e8f..57a32135 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/EnumTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/EnumTypeHandlerTests.cs @@ -80,7 +80,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateCorrectCode_ForNonNulla var result = _handler.GenerateToAttributeValue(property); - var expected = "new AttributeValue { S = model.Status.ToString() }"; + var expected = "AttributeValue.String(model.Status.ToString())"; await Assert.That(result).IsEqualTo(expected); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/PrimitiveTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/PrimitiveTypeHandlerTests.cs index 77016664..b78c8b9c 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/PrimitiveTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/PrimitiveTypeHandlerTests.cs @@ -63,12 +63,12 @@ public async Task GenerateToAttributeValue_ShouldGenerateCorrectCode_ForNonNulla { var testCases = new[] { - (MockSymbolFactory.PrimitiveTypes.Boolean, "BoolProp", "new AttributeValue { BOOL = model.BoolProp }"), - (MockSymbolFactory.PrimitiveTypes.Int32, "IntProp", "new AttributeValue { N = model.IntProp.ToString(CultureInfo.InvariantCulture) }"), + (MockSymbolFactory.PrimitiveTypes.Boolean, "BoolProp", "AttributeValue.Bool(model.BoolProp)"), + (MockSymbolFactory.PrimitiveTypes.Int32, "IntProp", "AttributeValue.Number(model.IntProp.ToString(CultureInfo.InvariantCulture))"), (MockSymbolFactory.PrimitiveTypes.String, "StringProp", (string?)null), // Strings now use conditional assignment to skip empty strings - (MockSymbolFactory.PrimitiveTypes.Decimal, "DecimalProp", "new AttributeValue { N = model.DecimalProp.ToString(CultureInfo.InvariantCulture) }"), - (MockSymbolFactory.PrimitiveTypes.Double, "DoubleProp", "new AttributeValue { N = model.DoubleProp.ToString(CultureInfo.InvariantCulture) }"), - (MockSymbolFactory.PrimitiveTypes.Guid, "GuidProp", "new AttributeValue { S = model.GuidProp.ToString() }") + (MockSymbolFactory.PrimitiveTypes.Decimal, "DecimalProp", "AttributeValue.Number(model.DecimalProp.ToString(CultureInfo.InvariantCulture))"), + (MockSymbolFactory.PrimitiveTypes.Double, "DoubleProp", "AttributeValue.Number(model.DoubleProp.ToString(CultureInfo.InvariantCulture))"), + (MockSymbolFactory.PrimitiveTypes.Guid, "GuidProp", "AttributeValue.String(model.GuidProp.ToString())") }; foreach (var (type, propName, expectedCode) in testCases) diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/StringEmptyHandlingTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/StringEmptyHandlingTests.cs index ca79fe81..e17ab67a 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/StringEmptyHandlingTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/StringEmptyHandlingTests.cs @@ -75,7 +75,7 @@ await Assert.That(result) .Because("Non-nullable strings should check for both null and empty"); await Assert.That(result) - .Contains("record[\"Name\"] = new AttributeValue { S = model.Name };") + .Contains("record[\"Name\"] = AttributeValue.String(model.Name);") .Because("Should assign S attribute when string is not null or empty"); } @@ -101,7 +101,7 @@ await Assert.That(result) .Because("Nullable strings should check for both null and empty"); await Assert.That(result) - .Contains("record[\"Description\"] = new AttributeValue { S = model.Description };") + .Contains("record[\"Description\"] = AttributeValue.String(model.Description);") .Because("Should assign S attribute when string is not null or empty"); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/TimeOnlyTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/TimeOnlyTypeHandlerTests.cs index b7c6cdaf..dae73af1 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/TimeOnlyTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/TimeOnlyTypeHandlerTests.cs @@ -61,7 +61,7 @@ public async Task GenerateToAttributeValue_ShouldGenerateCorrectCode_ForNonNulla var result = _handler.GenerateToAttributeValue(property); - var expected = "new AttributeValue { S = model.StartTime.ToString(\"HH:mm:ss.fffffff\") }"; + var expected = "AttributeValue.String(model.StartTime.ToString(\"HH:mm:ss.fffffff\"))"; await Assert.That(result).IsEqualTo(expected); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/UnixTimestampTypeHandlerTests.cs b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/UnixTimestampTypeHandlerTests.cs index ac615862..6ca862c8 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/UnixTimestampTypeHandlerTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Generator.Tests/TypeHandlers/UnixTimestampTypeHandlerTests.cs @@ -107,7 +107,7 @@ public async Task GenerateToAttributeValue_NonNullableDateTime_SecondsFormat_Sho var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "new AttributeValue { N = ((DateTimeOffset)model.CreatedAt).ToUnixTimeSeconds().ToString() }"; + var expected = "AttributeValue.Number(((DateTimeOffset)model.CreatedAt).ToUnixTimeSeconds().ToString())"; await Assert.That(result) .IsEqualTo(expected); } @@ -127,7 +127,7 @@ public async Task GenerateToAttributeValue_NonNullableDateTime_MillisecondsForma var result = _handler.GenerateToAttributeValue(propertyInfo); // Assert - var expected = "new AttributeValue { N = ((DateTimeOffset)model.CreatedAt).ToUnixTimeMilliseconds().ToString() }"; + var expected = "AttributeValue.Number(((DateTimeOffset)model.CreatedAt).ToUnixTimeMilliseconds().ToString())"; await Assert.That(result) .IsEqualTo(expected); } diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs index 43fa2cf5..23c91b7b 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs @@ -121,8 +121,8 @@ public async Task QueryBuilder_WithExclusiveStartKey_SetsKey() { var startKey = new Dictionary { - ["pk"] = new AttributeValue { S = "lastPk" }, - ["sk"] = new AttributeValue { S = "lastSk" } + ["pk"] = AttributeValue.String("lastPk"), + ["sk"] = AttributeValue.String("lastSk") }; var builder = new QueryBuilder("TestTable") @@ -167,7 +167,7 @@ public async Task ScanBuilder_WithExclusiveStartKey_SetsKey() { var startKey = new Dictionary { - ["pk"] = new AttributeValue { S = "lastPk" } + ["pk"] = AttributeValue.String("lastPk") }; var builder = new ScanBuilder("TestTable") @@ -198,7 +198,7 @@ public async Task BatchWriteItemBuilder_WithReturnConsumedCapacity_SetsCapacity( { var builder = new BatchWriteItemBuilder() .WithTable("TestTable", table => table - .WithPut(new Dictionary { ["pk"] = new AttributeValue { S = "value1" } })) + .WithPut(new Dictionary { ["pk"] = AttributeValue.String("value1") })) .WithReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL) .WithReturnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE); @@ -212,7 +212,7 @@ public async Task BatchWriteItemBuilder_WithReturnConsumedCapacity_SetsCapacity( public async Task DeleteItemBuilder_WithReturnConsumedCapacity_SetsCapacity() { var builder = new DeleteItemBuilder("TestTable") - .WithKey("pk", new AttributeValue { S = "value1" }) + .WithKey("pk", AttributeValue.String("value1")) .WithReturnConsumedCapacity(ReturnConsumedCapacity.INDEXES) .WithReturnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE); @@ -226,7 +226,7 @@ public async Task DeleteItemBuilder_WithReturnConsumedCapacity_SetsCapacity() public async Task DeleteItemBuilder_WithReturnValuesOnConditionCheckFailure_AllOld_SetsValue() { var builder = new DeleteItemBuilder("TestTable") - .WithKey("pk", new AttributeValue { S = "value1" }) + .WithKey("pk", AttributeValue.String("value1")) .WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD); var request = builder.Build(); @@ -238,7 +238,7 @@ public async Task DeleteItemBuilder_WithReturnValuesOnConditionCheckFailure_AllO public async Task DeleteItemBuilder_WithReturnValuesOnConditionCheckFailure_None_SetsValue() { var builder = new DeleteItemBuilder("TestTable") - .WithKey("pk", new AttributeValue { S = "value1" }) + .WithKey("pk", AttributeValue.String("value1")) .WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.NONE); var request = builder.Build(); @@ -274,7 +274,7 @@ public async Task PutItemBuilder_WithReturnValuesOnConditionCheckFailure_None_Se public async Task UpdateItemBuilder_WithReturnValuesOnConditionCheckFailure_AllOld_SetsValue() { var builder = new UpdateItemBuilder("TestTable") - .WithKey("pk", new AttributeValue { S = "value1" }) + .WithKey("pk", AttributeValue.String("value1")) .Set("status", "active") .WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD); @@ -287,7 +287,7 @@ public async Task UpdateItemBuilder_WithReturnValuesOnConditionCheckFailure_AllO public async Task UpdateItemBuilder_WithReturnValuesOnConditionCheckFailure_None_SetsValue() { var builder = new UpdateItemBuilder("TestTable") - .WithKey("pk", new AttributeValue { S = "value1" }) + .WithKey("pk", AttributeValue.String("value1")) .Set("status", "active") .WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.NONE); @@ -300,7 +300,7 @@ public async Task UpdateItemBuilder_WithReturnValuesOnConditionCheckFailure_None public async Task DeleteItemBuilder_WithReturnValuesOnConditionCheckFailure_AllOld_SerializesCorrectly() { var builder = new DeleteItemBuilder("TestTable") - .WithKey("pk", new AttributeValue { S = "value1" }) + .WithKey("pk", AttributeValue.String("value1")) .WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD); var request = builder.Build(); @@ -326,7 +326,7 @@ public async Task PutItemBuilder_WithReturnValuesOnConditionCheckFailure_AllOld_ public async Task UpdateItemBuilder_WithReturnValuesOnConditionCheckFailure_AllOld_SerializesCorrectly() { var builder = new UpdateItemBuilder("TestTable") - .WithKey("pk", new AttributeValue { S = "value1" }) + .WithKey("pk", AttributeValue.String("value1")) .Set("status", "active") .WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD); diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoClientIntegrationTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoClientIntegrationTests.cs index 3874cbba..aa76f81c 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoClientIntegrationTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoClientIntegrationTests.cs @@ -25,10 +25,10 @@ public async Task PutItemAsync_ShouldStoreItem_WhenValidItemProvided() { var item = new Dictionary { - ["pk"] = new() { S = "test-pk" }, - ["sk"] = new() { S = "test-sk" }, - ["data"] = new() { S = "test-data" }, - ["number"] = new() { N = "123" } + ["pk"] = AttributeValue.String("test-pk"), + ["sk"] = AttributeValue.String("test-sk"), + ["data"] = AttributeValue.String("test-data"), + ["number"] = AttributeValue.Number("123") }; var request = new PutItemRequest @@ -47,9 +47,9 @@ public async Task GetItemAsync_ShouldReturnItem_WhenItemExists() { var putItem = new Dictionary { - ["pk"] = new() { S = "get-test-pk" }, - ["sk"] = new() { S = "get-test-sk" }, - ["data"] = new() { S = "retrieved-data" } + ["pk"] = AttributeValue.String("get-test-pk"), + ["sk"] = AttributeValue.String("get-test-sk"), + ["data"] = AttributeValue.String("retrieved-data") }; var putRequest = new PutItemRequest @@ -65,8 +65,8 @@ public async Task GetItemAsync_ShouldReturnItem_WhenItemExists() TableName = _fixture.TestTableName, Key = new Dictionary { - ["pk"] = new() { S = "get-test-pk" }, - ["sk"] = new() { S = "get-test-sk" } + ["pk"] = AttributeValue.String("get-test-pk"), + ["sk"] = AttributeValue.String("get-test-sk") } }; @@ -76,7 +76,7 @@ public async Task GetItemAsync_ShouldReturnItem_WhenItemExists() await Assert.That(() => response.Value.Item).IsNotNull(); var attribute = (response.Value.Item!)["data"]; await Assert.That(attribute).IsNotNull(); - await Assert.That(attribute!.S).IsEqualTo("retrieved-data"); + await Assert.That(attribute!.Value.S).IsEqualTo("retrieved-data"); } [Test] @@ -87,8 +87,8 @@ public async Task GetItemAsync_ShouldReturnEmptyItem_WhenItemDoesNotExist() TableName = _fixture.TestTableName, Key = new Dictionary { - ["pk"] = new() { S = "non-existent-pk" }, - ["sk"] = new() { S = "non-existent-sk" } + ["pk"] = AttributeValue.String("non-existent-pk"), + ["sk"] = AttributeValue.String("non-existent-sk") } }; @@ -103,10 +103,10 @@ public async Task UpdateItemAsync_ShouldModifyItem_WhenItemExists() { var initialItem = new Dictionary { - ["pk"] = new() { S = "update-test-pk" }, - ["sk"] = new() { S = "update-test-sk" }, - ["data"] = new() { S = "initial-data" }, - ["counter"] = new() { N = "1" } + ["pk"] = AttributeValue.String("update-test-pk"), + ["sk"] = AttributeValue.String("update-test-sk"), + ["data"] = AttributeValue.String("initial-data"), + ["counter"] = AttributeValue.Number("1") }; var putRequest = new PutItemRequest @@ -122,8 +122,8 @@ public async Task UpdateItemAsync_ShouldModifyItem_WhenItemExists() TableName = _fixture.TestTableName, Key = new Dictionary { - ["pk"] = new() { S = "update-test-pk" }, - ["sk"] = new() { S = "update-test-sk" } + ["pk"] = AttributeValue.String("update-test-pk"), + ["sk"] = AttributeValue.String("update-test-sk") }, UpdateExpression = "SET #data = :newData, #counter = #counter + :inc", ExpressionAttributeNames = new Dictionary @@ -133,8 +133,8 @@ public async Task UpdateItemAsync_ShouldModifyItem_WhenItemExists() }, ExpressionAttributeValues = new Dictionary { - [":newData"] = new() { S = "updated-data" }, - [":inc"] = new() { N = "1" } + [":newData"] = AttributeValue.String("updated-data"), + [":inc"] = AttributeValue.Number("1") }, ReturnValues = ReturnValues.ALL_NEW }; @@ -156,9 +156,9 @@ public async Task DeleteItemAsync_ShouldRemoveItem_WhenItemExists() { var item = new Dictionary { - ["pk"] = new() { S = "delete-test-pk" }, - ["sk"] = new() { S = "delete-test-sk" }, - ["data"] = new() { S = "to-be-deleted" } + ["pk"] = AttributeValue.String("delete-test-pk"), + ["sk"] = AttributeValue.String("delete-test-sk"), + ["data"] = AttributeValue.String("to-be-deleted") }; var putRequest = new PutItemRequest @@ -174,8 +174,8 @@ public async Task DeleteItemAsync_ShouldRemoveItem_WhenItemExists() TableName = _fixture.TestTableName, Key = new Dictionary { - ["pk"] = new() { S = "delete-test-pk" }, - ["sk"] = new() { S = "delete-test-sk" } + ["pk"] = AttributeValue.String("delete-test-pk"), + ["sk"] = AttributeValue.String("delete-test-sk") }, ReturnValues = ReturnValues.ALL_OLD }; @@ -192,8 +192,8 @@ public async Task DeleteItemAsync_ShouldRemoveItem_WhenItemExists() TableName = _fixture.TestTableName, Key = new Dictionary { - ["pk"] = new() { S = "delete-test-pk" }, - ["sk"] = new() { S = "delete-test-sk" } + ["pk"] = AttributeValue.String("delete-test-pk"), + ["sk"] = AttributeValue.String("delete-test-sk") } }; @@ -208,21 +208,21 @@ public async Task QueryAsync_ShouldReturnMatchingItems_WhenItemsExist() { new Dictionary { - ["pk"] = new() { S = "query-test-pk" }, - ["sk"] = new() { S = "item1" }, - ["data"] = new() { S = "data1" } + ["pk"] = AttributeValue.String("query-test-pk"), + ["sk"] = AttributeValue.String("item1"), + ["data"] = AttributeValue.String("data1") }, new Dictionary { - ["pk"] = new() { S = "query-test-pk" }, - ["sk"] = new() { S = "item2" }, - ["data"] = new() { S = "data2" } + ["pk"] = AttributeValue.String("query-test-pk"), + ["sk"] = AttributeValue.String("item2"), + ["data"] = AttributeValue.String("data2") }, new Dictionary { - ["pk"] = new() { S = "different-pk" }, - ["sk"] = new() { S = "item3" }, - ["data"] = new() { S = "data3" } + ["pk"] = AttributeValue.String("different-pk"), + ["sk"] = AttributeValue.String("item3"), + ["data"] = AttributeValue.String("data3") } }; @@ -242,7 +242,7 @@ public async Task QueryAsync_ShouldReturnMatchingItems_WhenItemsExist() KeyConditionExpression = "pk = :pk", ExpressionAttributeValues = new Dictionary { - [":pk"] = new() { S = "query-test-pk" } + [":pk"] = AttributeValue.String("query-test-pk") } }; @@ -260,15 +260,15 @@ public async Task ScanAsync_ShouldReturnAllItems_WhenNoFilterApplied() { new Dictionary { - ["pk"] = new() { S = "scan-test-pk1" }, - ["sk"] = new() { S = "scan-item1" }, - ["type"] = new() { S = "test" } + ["pk"] = AttributeValue.String("scan-test-pk1"), + ["sk"] = AttributeValue.String("scan-item1"), + ["type"] = AttributeValue.String("test") }, new Dictionary { - ["pk"] = new() { S = "scan-test-pk2" }, - ["sk"] = new() { S = "scan-item2" }, - ["type"] = new() { S = "test" } + ["pk"] = AttributeValue.String("scan-test-pk2"), + ["sk"] = AttributeValue.String("scan-item2"), + ["type"] = AttributeValue.String("test") } }; @@ -292,7 +292,7 @@ public async Task ScanAsync_ShouldReturnAllItems_WhenNoFilterApplied() }, ExpressionAttributeValues = new Dictionary { - [":type"] = new() { S = "test" } + [":type"] = AttributeValue.String("test") } }; @@ -310,15 +310,15 @@ public async Task BatchGetItemAsync_ShouldReturnRequestedItems_WhenItemsExist() { new Dictionary { - ["pk"] = new() { S = "batch-get-pk1" }, - ["sk"] = new() { S = "batch-get-sk1" }, - ["data"] = new() { S = "batch-data1" } + ["pk"] = AttributeValue.String("batch-get-pk1"), + ["sk"] = AttributeValue.String("batch-get-sk1"), + ["data"] = AttributeValue.String("batch-data1") }, new Dictionary { - ["pk"] = new() { S = "batch-get-pk2" }, - ["sk"] = new() { S = "batch-get-sk2" }, - ["data"] = new() { S = "batch-data2" } + ["pk"] = AttributeValue.String("batch-get-pk2"), + ["sk"] = AttributeValue.String("batch-get-sk2"), + ["data"] = AttributeValue.String("batch-data2") } }; @@ -342,13 +342,13 @@ public async Task BatchGetItemAsync_ShouldReturnRequestedItems_WhenItemsExist() { new() { - ["pk"] = new() { S = "batch-get-pk1" }, - ["sk"] = new() { S = "batch-get-sk1" } + ["pk"] = AttributeValue.String("batch-get-pk1"), + ["sk"] = AttributeValue.String("batch-get-sk1") }, new() { - ["pk"] = new() { S = "batch-get-pk2" }, - ["sk"] = new() { S = "batch-get-sk2" } + ["pk"] = AttributeValue.String("batch-get-pk2"), + ["sk"] = AttributeValue.String("batch-get-sk2") } } } @@ -366,16 +366,16 @@ public async Task BatchWriteItemAsync_ShouldProcessMultipleOperations_WhenValidR { var putItem = new Dictionary { - ["pk"] = new() { S = "batch-write-pk1" }, - ["sk"] = new() { S = "batch-write-sk1" }, - ["data"] = new() { S = "batch-put-data" } + ["pk"] = AttributeValue.String("batch-write-pk1"), + ["sk"] = AttributeValue.String("batch-write-sk1"), + ["data"] = AttributeValue.String("batch-put-data") }; var existingItem = new Dictionary { - ["pk"] = new() { S = "batch-write-pk2" }, - ["sk"] = new() { S = "batch-write-sk2" }, - ["data"] = new() { S = "to-be-deleted" } + ["pk"] = AttributeValue.String("batch-write-pk2"), + ["sk"] = AttributeValue.String("batch-write-sk2"), + ["data"] = AttributeValue.String("to-be-deleted") }; var putExistingRequest = new PutItemRequest @@ -401,8 +401,8 @@ public async Task BatchWriteItemAsync_ShouldProcessMultipleOperations_WhenValidR { Key = new Dictionary { - ["pk"] = new() { S = "batch-write-pk2" }, - ["sk"] = new() { S = "batch-write-sk2" } + ["pk"] = AttributeValue.String("batch-write-pk2"), + ["sk"] = AttributeValue.String("batch-write-sk2") } } } @@ -419,8 +419,8 @@ public async Task BatchWriteItemAsync_ShouldProcessMultipleOperations_WhenValidR TableName = _fixture.TestTableName, Key = new Dictionary { - ["pk"] = new() { S = "batch-write-pk1" }, - ["sk"] = new() { S = "batch-write-sk1" } + ["pk"] = AttributeValue.String("batch-write-pk1"), + ["sk"] = AttributeValue.String("batch-write-sk1") } }; @@ -432,12 +432,58 @@ public async Task BatchWriteItemAsync_ShouldProcessMultipleOperations_WhenValidR TableName = _fixture.TestTableName, Key = new Dictionary { - ["pk"] = new() { S = "batch-write-pk2" }, - ["sk"] = new() { S = "batch-write-sk2" } + ["pk"] = AttributeValue.String("batch-write-pk2"), + ["sk"] = AttributeValue.String("batch-write-sk2") } }; var getDeletedResponse = await _fixture.DynamoClient.GetItemAsync(getDeletedRequest); await Assert.That(() => getDeletedResponse.Value.Item).IsNull(); } + + [Test] + public async Task QueryAsync_ShouldPaginate_WhenLimitIsSet() + { + // Insert 10 items + for (var i = 0; i < 10; i++) + { + await _fixture.DynamoClient.PutItemAsync(new PutItemRequest + { + TableName = _fixture.TestTableName, + Item = new Dictionary + { + ["pk"] = AttributeValue.String("paginate-test"), + ["sk"] = AttributeValue.String($"item-{i:D4}"), + ["data"] = AttributeValue.String($"value-{i}") + } + }); + } + + var totalCount = 0; + var pageCount = 0; + Dictionary? lastKey = null; + + do + { + var response = await _fixture.DynamoClient.QueryAsync(new QueryRequest + { + TableName = _fixture.TestTableName, + KeyConditionExpression = "pk = :pk", + ExpressionAttributeValues = new Dictionary + { + [":pk"] = AttributeValue.String("paginate-test") + }, + Limit = 3, + ExclusiveStartKey = lastKey + }); + + await Assert.That(response.IsError).IsFalse(); + totalCount += response.Value.Items.Count; + pageCount++; + lastKey = response.Value.HasMoreResults ? response.Value.LastEvaluatedKey : null; + } while (lastKey != null); + + await Assert.That(totalCount).IsEqualTo(10); + await Assert.That(pageCount).IsGreaterThanOrEqualTo(4); // 10 items / 3 per page = at least 4 pages + } } diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoRecordExtensionsTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoRecordExtensionsTests.cs index 8d41878d..7411babe 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoRecordExtensionsTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoRecordExtensionsTests.cs @@ -10,7 +10,7 @@ public async Task TryGetString_EmptyString_ShouldReturnTrueWithEmptyString() { var record = new DynamoRecord { - ["Name"] = new AttributeValue { S = "" } + ["Name"] = AttributeValue.String("") }; var result = record.TryGetString("Name", out var value); @@ -26,7 +26,7 @@ public async Task TryGetNullableString_EmptyString_ShouldReturnTrueWithEmptyStri { var record = new DynamoRecord { - ["Name"] = new AttributeValue { S = "" } + ["Name"] = AttributeValue.String("") }; var result = record.TryGetNullableString("Name", out var value); @@ -42,7 +42,7 @@ public async Task TryGetString_NullS_ShouldReturnFalse() { var record = new DynamoRecord { - ["Name"] = new AttributeValue { NULL = true } + ["Name"] = AttributeValue.Null() }; var result = record.TryGetString("Name", out _); @@ -56,7 +56,7 @@ public async Task TryGetNullableString_NullS_ShouldReturnTrueWithNull() { var record = new DynamoRecord { - ["Name"] = new AttributeValue { NULL = true } + ["Name"] = AttributeValue.Null() }; var result = record.TryGetNullableString("Name", out var value); diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoResponseReaderTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoResponseReaderTests.cs index 48f92050..53ded7a0 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoResponseReaderTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoResponseReaderTests.cs @@ -830,6 +830,64 @@ public async Task ReadGetItemResponse_RefOverload_ShouldReturnNull_WhenNoItemPro await Assert.That(result).IsNull(); } + // === NULL attribute validation === + + [Test] + public async Task ReadAttributeValue_NullTrue_ShouldReturnNullAttributeValue() + { + var json = """ + { + "Items": [], + "Count": 0, + "ScannedCount": 0, + "LastEvaluatedKey": { + "empty": {"NULL": true} + } + } + """u8; + + var result = DynamoResponseReader.ReadQueryResponse(json, ReadTestEntity); + + await Assert.That(result.LastEvaluatedKey).IsNotNull(); + await Assert.That(result.LastEvaluatedKey!["empty"].NULL).IsEqualTo(true); + } + + [Test] + public void ReadAttributeValue_NullFalse_ShouldThrowJsonException() + { + var json = System.Text.Encoding.UTF8.GetBytes(""" + { + "Items": [], + "Count": 0, + "ScannedCount": 0, + "LastEvaluatedKey": { + "empty": {"NULL": false} + } + } + """); + + Assert.Throws(() => DynamoResponseReader.ReadQueryResponse(json, ReadTestEntity)); + } + + // === Wrapper object validation === + + [Test] + public void ReadAttributeValue_ExtraPropertyInWrapper_ShouldThrowJsonException() + { + var json = System.Text.Encoding.UTF8.GetBytes(""" + { + "Items": [], + "Count": 0, + "ScannedCount": 0, + "LastEvaluatedKey": { + "pk": {"S": "value", "Extra": "bad"} + } + } + """); + + Assert.Throws(() => DynamoResponseReader.ReadQueryResponse(json, ReadTestEntity)); + } + // === Binary attribute parsing === [Test] diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs index 3d80fded..f362a66a 100644 --- a/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs +++ b/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs @@ -16,17 +16,17 @@ public async Task QueryAllAsync_DynamoRecord_PaginatesAutomatically() var mock = new Mock(); var lastKey = new Dictionary { - ["pk"] = new AttributeValue { S = "lastPk" } + ["pk"] = AttributeValue.String("lastPk") }; ErrorOr firstPage = new QueryResponse { - Items = [new DynamoRecord { ["pk"] = new AttributeValue { S = "pk1" } }], + Items = [new DynamoRecord { ["pk"] = AttributeValue.String("pk1") }], LastEvaluatedKey = lastKey }; ErrorOr secondPage = new QueryResponse { - Items = [new DynamoRecord { ["pk"] = new AttributeValue { S = "pk2" } }], + Items = [new DynamoRecord { ["pk"] = AttributeValue.String("pk2") }], LastEvaluatedKey = null }; @@ -48,17 +48,17 @@ public async Task ScanAllAsync_DynamoRecord_PaginatesAutomatically() var mock = new Mock(); var lastKey = new Dictionary { - ["pk"] = new AttributeValue { S = "lastPk" } + ["pk"] = AttributeValue.String("lastPk") }; ErrorOr firstPage = new ScanResponse { - Items = [new DynamoRecord { ["pk"] = new AttributeValue { S = "pk1" } }], + Items = [new DynamoRecord { ["pk"] = AttributeValue.String("pk1") }], LastEvaluatedKey = lastKey }; ErrorOr secondPage = new ScanResponse { - Items = [new DynamoRecord { ["pk"] = new AttributeValue { S = "pk2" } }], + Items = [new DynamoRecord { ["pk"] = AttributeValue.String("pk2") }], LastEvaluatedKey = null };