Skip to content
Merged
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
195 changes: 195 additions & 0 deletions .agents/skills/new-event-source/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
---
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is for us (developers) so we can easily add new events in future

name: new-event-source
description: Add a new AWS event source attribute (e.g., Kinesis, Kafka, MQ) to the Lambda .NET Annotations framework, including the attribute class, source generator integration, CloudFormation writer, unit tests, writer tests, source generator tests, and integration tests
---

# Adding a New Event Source to Lambda Annotations

This skill guides you through adding a complete new event source attribute to the AWS Lambda .NET Annotations framework. Use this when a user asks to add support for a new AWS event source like Kinesis, Kafka, MQ, etc.

## Prerequisites

Before starting, gather from the user:
1. **Service name** (e.g., "Kinesis", "Kafka", "MQ")
2. **Primary resource identifier** (e.g., stream ARN, topic ARN, broker ARN)
3. **CloudFormation event type string** (e.g., "Kinesis", "MSK", "MQ")
4. **Event class name** from the corresponding `Amazon.Lambda.*Events` NuGet package (e.g., `KinesisEvent`)
5. **Optional properties** the attribute should support (e.g., BatchSize, StartingPosition, Filters)
6. **Whether `@` references use `Fn::GetAtt` or `Ref`** — event source mappings use `Fn::GetAtt`, subscriptions use `Ref`

## Reference Examples

Read these files to understand existing patterns before creating new ones:
- **SNS (simplest, subscription-based)**: `Libraries/src/Amazon.Lambda.Annotations/SNS/SNSEventAttribute.cs`
- **SQS (event source mapping with batching)**: `Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs`
- **DynamoDB (stream-based)**: `Libraries/src/Amazon.Lambda.Annotations/DynamoDB/DynamoDBEventAttribute.cs`
- **S3 (notification-based)**: `Libraries/src/Amazon.Lambda.Annotations/S3/S3EventAttribute.cs`

## Steps

### Step 1: Create the Event Attribute Class

**Create**: `Libraries/src/Amazon.Lambda.Annotations/{ServiceName}/{ServiceName}EventAttribute.cs`

Key patterns:
- Add copyright header: `// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.` + `// SPDX-License-Identifier: Apache-2.0`
- Inherit from `Attribute` with `[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]`
- Constructor takes the primary resource identifier as a required `string` parameter
- All optional properties use nullable backing fields with `Is<PropertyName>Set` internal properties
- Include auto-derived `ResourceName` property (strips `@` prefix or extracts name from ARN)
- Include `internal List<string> Validate()` method with all validation rules
- Use `Regex("^[a-zA-Z0-9]+$")` for ResourceName validation

### Step 2: Register Type Full Names

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs`

Add constants:
```csharp
public const string {ServiceName}EventAttribute = "Amazon.Lambda.Annotations.{ServiceName}.{ServiceName}EventAttribute";
public const string {ServiceName}Event = "Amazon.Lambda.{ServiceName}Events.{ServiceName}Event";
```

Also add to `EventType` enum if needed in `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs`.

### Step 3: Create the Attribute Builder

**Create**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/{ServiceName}EventAttributeBuilder.cs`

Extracts attribute data from Roslyn `AttributeData`. Use consistent `else if` chaining. Reference: `SNSEventAttributeBuilder.cs`.

### Step 4: Register in AttributeModelBuilder

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs`

Add `else if` block for the new attribute type after the existing event attribute blocks.

### Step 5: Register in EventTypeBuilder

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs`

Add `else if` block mapping the attribute to the `EventType` enum value.

### Step 6: Add DiagnosticDescriptor

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs`

Add descriptor with the next available `AWSLambda0XXX` ID for invalid attribute validation errors.

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md` — add the new diagnostic ID.

### Step 7: Add Validation in LambdaFunctionValidator

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs`

1. Add `Validate{ServiceName}Events()` call in `ValidateFunction` method
2. Create private `Validate{ServiceName}Events()` method that validates:
- Attribute properties via `Validate()` method
- Method parameters (first must be event type, optional second is `ILambdaContext`)
- Return type (usually `void` or `Task`)

### Step 8: Add Dependency Check

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs`

In `ValidateDependencies`, add check for `Amazon.Lambda.{ServiceName}Events` NuGet package.

### Step 9: Check SyntaxReceiver

**Check**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs`

Add the new attribute name if the SyntaxReceiver filters by attribute name strings.

### Step 10: Add CloudFormation Writer Logic

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs`

1. Add `case AttributeModel<{ServiceName}EventAttribute>` in the event processing switch
2. Create `Process{ServiceName}Attribute()` method that writes CF template properties
- Event source mappings (SQS, DynamoDB, Kinesis): use `Fn::GetAtt` for `@` references
- Subscription events (SNS): use `Ref` for `@` references
- Track synced properties in metadata

### Step 11: Create Attribute Unit Tests

**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/{ServiceName}EventAttributeTests.cs`

Cover: constructor, defaults, property tracking, ResourceName derivation, all validation paths. Reference: `SQSEventAttributeTests.cs`, `DynamoDBEventAttributeTests.cs`, `SNSEventAttributeTests.cs`.

### Step 12: Create CloudFormation Writer Tests

**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/{ServiceName}EventsTests.cs`

This is a `partial class CloudFormationWriterTests`. Include tests for:
1. `Verify{ServiceName}EventAttributes_AreCorrectlyApplied` — Theory with JSON/YAML and property combinations
2. `Verify{ServiceName}EventProperties_AreSyncedCorrectly` — Synced properties update when attributes change
3. `SwitchBetweenArnAndRef_For{Resource}` — ARN to `@` reference switching
4. `Verify{Resource}CanBeSet_FromCloudFormationParameter` — CF Parameters handling
5. `VerifyManuallySet{ServiceName}EventProperties_ArePreserved` — Hand-edited template preservation

Reference: `SQSEventsTests.cs`, `DynamoDBEventsTests.cs`, `SNSEventsTests.cs`.

### Step 13: Create Valid Event Examples + Source Generator Test

**Create**: `Libraries/test/TestServerlessApp/{ServiceName}EventExamples/Valid{ServiceName}Events.cs.txt`
**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/` (generated handler snapshots)
**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/{serviceName}Events.template`
**Modify**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` — add `VerifyValid{ServiceName}Events()` test

### Step 14: Create Invalid Event Examples + Source Generator Test

**Create**: `Libraries/test/TestServerlessApp/{ServiceName}EventExamples/Invalid{ServiceName}Events.cs.error`

Cover: invalid property values, invalid params, invalid return type, multiple events, invalid ARN, invalid resource name.

**Modify**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` — add `VerifyInvalid{ServiceName}Events_ThrowsCompilationErrors()` test with diagnostic assertions including line spans.

### Step 15: Create Generated Code Snapshots

**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/`

Tip: Run the source generator once to get actual output, then use as snapshot.

### Step 16: Create Integration Test

**Create**: `Libraries/test/TestServerlessApp.IntegrationTests/{ServiceName}EventSourceMapping.cs`
**Modify**: `Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs` — resource lookup
**Modify**: `Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1` — if needed

### Step 17: Update AnalyzerReleases.Unshipped.md

**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md`

## File Map Summary

| Action | File Path |
|--------|-----------|
| Create | `src/Amazon.Lambda.Annotations/{ServiceName}/{ServiceName}EventAttribute.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs` |
| Create | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/{ServiceName}EventAttributeBuilder.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs` |
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md` |
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/{ServiceName}EventAttributeTests.cs` |
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/{ServiceName}EventsTests.cs` |
| Create | `test/TestServerlessApp/{ServiceName}EventExamples/Valid{ServiceName}Events.cs.txt` |
| Create | `test/TestServerlessApp/{ServiceName}EventExamples/Invalid{ServiceName}Events.cs.error` |
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/` |
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/{serviceName}Events.template` |
| Modify | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` |
| Create | `test/TestServerlessApp.IntegrationTests/{ServiceName}EventSourceMapping.cs` |
| Modify | `test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs` |

## Important Conventions

- **Copyright header** on every new `.cs` file
- **Consistent `else if` chaining** in attribute builders (never `if` then `if` for the same loop)
- **Both JSON and YAML** template formats must be tested in writer tests
- **Invalid event test spans** must reference exact line numbers in the `.cs.error` file
- **`.cs.txt` extension** for valid test files (prevents deployment)
- **`.cs.error` extension** for invalid test files (prevents compilation)
- **Use enums instead of strings** when you need to represent a fixed, known set of constants that do not change frequently (e.g., `StartingPosition`, `AuthType`, `HttpApiVersion`). Enums provide compile-time type safety, eliminate the need for manual string validation in the `Validate()` method, and prevent invalid values from being set. In attribute builders, enum values come through Roslyn's `AttributeData` as their underlying `int` value and must be cast accordingly (e.g., `(MyEnum)(int)pair.Value.Value`). When writing enum values to CloudFormation templates, use `.ToString()` to convert back to the string representation.
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,4 +21,5 @@ 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
AWSLambda0138 | AWSLambdaCSharpGenerator | Error | Invalid SNSEventAttribute
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ public static class DiagnosticDescriptors
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 thread
GarrettBeatty marked this conversation as resolved.

public static readonly DiagnosticDescriptor InvalidSnsEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0138",
title: "Invalid SNSEventAttribute",
messageFormat: "Invalid SNSEventAttribute encountered: {0}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.DynamoDB;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -113,6 +114,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.SNSEventAttribute), SymbolEqualityComparer.Default))
{
var data = SNSEventAttributeBuilder.Build(att);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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;
}
else 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 int startingPosition)
{
data.StartingPosition = (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
@@ -1,4 +1,7 @@
using Amazon.Lambda.Annotations.SQS;
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
using System;

Expand All @@ -24,7 +27,7 @@ public static SQSEventAttribute Build(AttributeData att)
{
data.ResourceName = resourceName;
}
if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize)
else if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize)
{
data.BatchSize = batchSize;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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.SNSEventAttribute)
{
events.Add(EventType.SNS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "SQSEventAttribute", "SQSEvent" },
{ "ALBApiAttribute", "ALBApi" },
{ "S3EventAttribute", "S3Event" },
{ "DynamoDBEventAttribute", "DynamoDBEvent" },
{ "SNSEventAttribute", "SNSEvent" }
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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 SNSEvent = "Amazon.Lambda.SNSEvents.SNSEvent";
public const string SNSEventAttribute = "Amazon.Lambda.Annotations.SNS.SNSEventAttribute";

Expand Down Expand Up @@ -95,6 +98,7 @@ public static class TypeFullNames
SQSEventAttribute,
ALBApiAttribute,
S3EventAttribute,
DynamoDBEventAttribute,
SNSEventAttribute
};
}
Expand Down
Loading
Loading