Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .autover/changes/add-dynamodbevent-annotation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.Annotations",
"Type": "Minor",
"ChangelogMessages": [
"Added [DynamoDBEvent] annotation attribute for declaratively configuring DynamoDB stream-triggered Lambda functions with support for stream reference, batch size, starting position, batching window, filters, and enabled state."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ AWSLambda0133 | AWSLambdaCSharpGenerator | Error | ALB Listener Reference Not Fo
AWSLambda0134 | AWSLambdaCSharpGenerator | Error | FromRoute not supported on ALB functions
AWSLambda0135 | AWSLambdaCSharpGenerator | Error | Unmapped parameter on ALB function
AWSLambda0136 | AWSLambdaCSharpGenerator | Error | Invalid S3EventAttribute
AWSLambda0137 | AWSLambdaCSharpGenerator | Error | Invalid DynamoDBEventAttribute
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,12 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidDynamoDBEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0137",
title: "Invalid DynamoDBEventAttribute",
messageFormat: "Invalid DynamoDBEventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
Comment on lines +285 to +290
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions a new diagnostic AWSLambda0132 for invalid DynamoDBEventAttribute, but in code the new diagnostic is AWSLambda0137 (and AWSLambda0132 is already used for Invalid ALBApiAttribute). Update the PR description (or the diagnostic ID, if the description is authoritative) to avoid confusion for users.

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.DynamoDB;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
Expand Down Expand Up @@ -101,6 +102,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.DynamoDBEventAttribute), SymbolEqualityComparer.Default))
{
var data = DynamoDBEventAttributeBuilder.Build(att);
model = new AttributeModel<DynamoDBEventAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAuthorizerAttribute), SymbolEqualityComparer.Default))
{
var data = HttpApiAuthorizerAttributeBuilder.Build(att);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Amazon.Lambda.Annotations.DynamoDB;
using Microsoft.CodeAnalysis;
using System;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
/// <summary>
/// Builder for <see cref="DynamoDBEventAttribute"/>.
/// </summary>
public class DynamoDBEventAttributeBuilder
{
public static DynamoDBEventAttribute Build(AttributeData att)
{
if (att.ConstructorArguments.Length != 1)
{
throw new NotSupportedException($"{TypeFullNames.DynamoDBEventAttribute} must have constructor with 1 argument.");
}
var stream = att.ConstructorArguments[0].Value as string;
var data = new DynamoDBEventAttribute(stream);

foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName)
{
data.ResourceName = resourceName;
}
if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize)
{
data.BatchSize = batchSize;
}
else if (pair.Key == nameof(data.StartingPosition) && pair.Value.Value is string startingPosition)
{
data.StartingPosition = startingPosition;
}
else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled)
{
data.Enabled = enabled;
}
else if (pair.Key == nameof(data.MaximumBatchingWindowInSeconds) && pair.Value.Value is uint maximumBatchingWindowInSeconds)
{
data.MaximumBatchingWindowInSeconds = maximumBatchingWindowInSeconds;
}
else if (pair.Key == nameof(data.Filters) && pair.Value.Value is string filters)
{
data.Filters = filters;
}
}

return data;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
{
events.Add(EventType.S3);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.DynamoDBEventAttribute)
{
events.Add(EventType.DynamoDB);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "RestApiAttribute", "RestApi" },
{ "SQSEventAttribute", "SQSEvent" },
{ "ALBApiAttribute", "ALBApi" },
{ "S3EventAttribute", "S3Event" }
{ "S3EventAttribute", "S3Event" },
{ "DynamoDBEventAttribute", "DynamoDBEvent" }
};

public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public static class TypeFullNames
public const string S3Event = "Amazon.Lambda.S3Events.S3Event";
public const string S3EventAttribute = "Amazon.Lambda.Annotations.S3.S3EventAttribute";

public const string DynamoDBEvent = "Amazon.Lambda.DynamoDBEvents.DynamoDBEvent";
public const string DynamoDBEventAttribute = "Amazon.Lambda.Annotations.DynamoDB.DynamoDBEventAttribute";

public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute";
public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";

Expand Down Expand Up @@ -84,7 +87,8 @@ public static class TypeFullNames
HttpApiAttribute,
SQSEventAttribute,
ALBApiAttribute,
S3EventAttribute
S3EventAttribute,
DynamoDBEventAttribute
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.DynamoDB;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
Expand Down Expand Up @@ -61,6 +62,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
// Validate Events
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateDynamoDBEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateAlbEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics);

Expand Down Expand Up @@ -110,6 +112,16 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe
}
}

// Check for references to "Amazon.Lambda.DynamoDBEvents" if the Lambda method is annotated with DynamoDBEvent attribute.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.DynamoDBEventAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.DynamoDBEvents") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.DynamoDBEvents"));
return false;
}
}

return true;
}

Expand Down Expand Up @@ -420,6 +432,45 @@ private static void ValidateS3Events(LambdaFunctionModel lambdaFunctionModel, Lo
}
}

private static void ValidateDynamoDBEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
{
if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.DynamoDB))
{
return;
}

// Validate DynamoDBEventAttributes
foreach (var att in lambdaFunctionModel.Attributes)
{
if (att.Type.FullName != TypeFullNames.DynamoDBEventAttribute)
continue;

var dynamoDBEventAttribute = ((AttributeModel<DynamoDBEventAttribute>)att).Data;
var validationErrors = dynamoDBEventAttribute.Validate();
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidDynamoDBEventAttribute, methodLocation, errorMessage)));
}

// Validate method parameters - When using DynamoDBEventAttribute, the method signature must be (DynamoDBEvent evnt) or (DynamoDBEvent evnt, ILambdaContext context)
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
if (parameters.Count == 0 ||
parameters.Count > 2 ||
(parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.DynamoDBEvent) ||
(parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.DynamoDBEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext)))
{
var errorMessage = $"When using the {nameof(DynamoDBEventAttribute)}, the Lambda method can accept at most 2 parameters. " +
$"The first parameter is required and must be of type {TypeFullNames.DynamoDBEvent}. " +
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}

// Validate method return type - When using DynamoDBEventAttribute, the return type must be either void or Task
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
{
var errorMessage = $"When using the {nameof(DynamoDBEventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
}

private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List<Diagnostic> diagnostics)
{
var isValid = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.DynamoDB;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
Expand Down Expand Up @@ -232,6 +233,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
eventName = ProcessS3Attribute(lambdaFunction, s3AttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
case AttributeModel<DynamoDBEventAttribute> dynamoDBAttributeModel:
eventName = ProcessDynamoDBAttribute(lambdaFunction, dynamoDBAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
}
}

Expand Down Expand Up @@ -608,6 +613,68 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S
return att.ResourceName;
}

/// <summary>
/// Writes all properties associated with <see cref="DynamoDBEventAttribute"/> to the serverless template.
/// </summary>
private string ProcessDynamoDBAttribute(ILambdaFunctionSerializable lambdaFunction, DynamoDBEventAttribute att, Dictionary<string, List<string>> syncedEventProperties)
{
var eventName = att.ResourceName;
var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}";

_templateWriter.SetToken($"{eventPath}.Type", "DynamoDB");

// Stream
_templateWriter.RemoveToken($"{eventPath}.Properties.Stream");
if (!att.Stream.StartsWith("@"))
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Stream", att.Stream);
}
else
{
var resource = att.Stream.Substring(1);
if (_templateWriter.Exists($"{PARAMETERS}.{resource}"))
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Stream.{REF}", resource);
else
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Stream.{GET_ATTRIBUTE}", new List<string> { resource, "StreamArn" }, TokenType.List);
}

// StartingPosition
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "StartingPosition", att.StartingPosition);

// BatchSize
if (att.IsBatchSizeSet)
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "BatchSize", att.BatchSize);
}

// Enabled
if (att.IsEnabledSet)
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled);
}

// MaximumBatchingWindowInSeconds
if (att.IsMaximumBatchingWindowInSecondsSet)
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "MaximumBatchingWindowInSeconds", att.MaximumBatchingWindowInSeconds);
}

// FilterCriteria
if (att.IsFiltersSet)
{
const char SEPERATOR = ';';
var filters = att.Filters.Split(SEPERATOR).Select(x => x.Trim()).ToList();
var filterList = new List<Dictionary<string, string>>();
foreach (var filter in filters)
{
filterList.Add(new Dictionary<string, string> { { "Pattern", filter } });
}
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "FilterCriteria.Filters", filterList, TokenType.List);
}

return att.ResourceName;
}

/// <summary>
/// Writes all properties associated with <see cref="S3EventAttribute"/> to the serverless template.
/// </summary>
Expand Down
Loading
Loading