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-scheduleevent-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 [ScheduleEvent] annotation attribute for declaratively configuring schedule-triggered Lambda functions with support for rate and cron expressions, description, input, 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
AWSLambda0139 | AWSLambdaCSharpGenerator | Error | Invalid ScheduleEventAttribute
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 InvalidScheduleEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0139",
title: "Invalid ScheduleEventAttribute",
messageFormat: "Invalid ScheduleEventAttribute 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.

This new diagnostic descriptor uses ID AWSLambda0139, but the PR description calls out a new diagnostic AWSLambda0132 for ScheduleEventAttribute validation. Please reconcile the diagnostic ID in code vs. the PR description (and any external docs) so consumers know which ID to expect.

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.Schedule;
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.ScheduleEventAttribute), SymbolEqualityComparer.Default))
{
var data = ScheduleEventAttributeBuilder.Build(att);
model = new AttributeModel<ScheduleEventAttribute>
{
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,44 @@
using Amazon.Lambda.Annotations.Schedule;
using Microsoft.CodeAnalysis;
using System;

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

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.Description) && pair.Value.Value is string description)
{
data.Description = description;
}
else if (pair.Key == nameof(data.Input) && pair.Value.Value is string input)
{
data.Input = input;
}
else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled)
{
data.Enabled = enabled;
}
}

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.ScheduleEventAttribute)
{
events.Add(EventType.Schedule);
}
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" },
{ "ScheduleEventAttribute", "ScheduleEvent" }
};

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 ScheduledEvent = "Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent";
public const string ScheduleEventAttribute = "Amazon.Lambda.Annotations.Schedule.ScheduleEventAttribute";

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,
ScheduleEventAttribute
};
}
}
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.Schedule;
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);
ValidateScheduleEvents(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.CloudWatchEvents" if the Lambda method is annotated with ScheduleEvent attribute.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.ScheduleEventAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.CloudWatchEvents") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.CloudWatchEvents"));
return false;
}
}

return true;
}

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

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

foreach (var att in lambdaFunctionModel.Attributes)
{
if (att.Type.FullName != TypeFullNames.ScheduleEventAttribute)
continue;

var scheduleEventAttribute = ((AttributeModel<ScheduleEventAttribute>)att).Data;
var validationErrors = scheduleEventAttribute.Validate();
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidScheduleEventAttribute, methodLocation, errorMessage)));
}

// Validate method parameters
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
if (parameters.Count > 2 ||
(parameters.Count >= 1 && parameters[0].Type.FullName != TypeFullNames.ScheduledEvent) ||
(parameters.Count == 2 && parameters[1].Type.FullName != TypeFullNames.ILambdaContext))
{
var errorMessage = $"When using the {nameof(ScheduleEventAttribute)}, the Lambda method can accept at most 2 parameters. " +
$"The first parameter must be of type {TypeFullNames.ScheduledEvent}. " +
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
Comment on lines +452 to +462
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.

The schedule-event method signature validation currently allows parameters.Count == 0. That’s inconsistent with the SQS/S3 validators (which require the first parameter) and with the PR description that says the first parameter must be ScheduledEvent. Update the condition to treat zero parameters as invalid and adjust the error message accordingly.

Copilot uses AI. Check for mistakes.

// Validate return type - must be void or Task
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
{
var errorMessage = $"When using the {nameof(ScheduleEventAttribute)}, 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
Expand Up @@ -3,6 +3,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.Schedule;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
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<ScheduleEventAttribute> scheduleAttributeModel:
eventName = ProcessScheduleAttribute(lambdaFunction, scheduleAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
}
}

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

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

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

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

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

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

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

return att.ResourceName;
}

/// <summary>
/// Writes all properties associated with <see cref="S3EventAttribute"/> to the serverless template.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions Libraries/src/Amazon.Lambda.Annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Topics:
- [Amazon API Gateway example](#amazon-api-gateway-example)
- [Amazon S3 example](#amazon-s3-example)
- [SQS Event Example](#sqs-event-example)
- [Schedule Event Example](#schedule-event-example)
- [Application Load Balancer (ALB) Example](#application-load-balancer-alb-example)
- [Custom Lambda Authorizer Example](#custom-lambda-authorizer-example)
- [HTTP API Authorizer](#http-api-authorizer)
Expand Down Expand Up @@ -1073,6 +1074,25 @@ The `ALBApi` attribute requires an existing ALB listener. Here is a minimal exam

Then your Lambda function references `@MyListener` in the `ALBApi` attribute.

## Schedule Event Example
This example shows how to use the `ScheduleEvent` attribute to trigger a Lambda function on a schedule using EventBridge.

The `ScheduleEvent` attribute contains the following properties:
* **Schedule** (Required) - The schedule expression. Supports `rate()` and `cron()` expressions (e.g. `rate(5 minutes)`, `cron(0 12 * * ? *)`).
* **ResourceName** (Optional) - The CloudFormation resource name for the schedule event.
* **Description** (Optional) - A description for the schedule rule.
* **Input** (Optional) - A JSON string to pass as input to the Lambda function.
* **Enabled** (Optional) - If false, the schedule rule is disabled. Default is true.

```csharp
[LambdaFunction(ResourceName = "ScheduledHandler", Policies = "AWSLambdaBasicExecutionRole")]
[ScheduleEvent("rate(5 minutes)", ResourceName = "FiveMinuteSchedule", Description = "Runs every 5 minutes")]
public void HandleSchedule(ScheduledEvent evnt, ILambdaContext lambdaContext)
{
lambdaContext.Logger.Log($"Scheduled event received at {evnt.Time}");
}
```

## Custom Lambda Authorizer Example

Lambda Annotations supports defining custom Lambda authorizers using attributes. Custom authorizers let you control access to your API Gateway endpoints by running a Lambda function that validates tokens or request parameters before the target function is invoked. The source generator automatically wires up the authorizer resources and references in the CloudFormation template.
Expand Down Expand Up @@ -1420,6 +1440,8 @@ parameter to the `LambdaFunction` must be the event object and the event source
* Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. The authorizer name is automatically derived from the method name. Other functions reference it via `RestApi.Authorizer` using `nameof()`. Use the `Type` property to choose between `Token` and `Request` authorizer types.
* SQSEvent
* Sets up event source mapping between the Lambda function and SQS queues. The SQS queue ARN is required to be set on the attribute. If users want to pass a reference to an existing SQS queue resource defined in their CloudFormation template, they can pass the SQS queue resource name prefixed with the '@' symbol.
* ScheduleEvent
* Triggers the Lambda function on a schedule using EventBridge. Supports rate and cron expressions.
* ALBApi
* Configures the Lambda function to be called from an Application Load Balancer. The listener ARN (or `@ResourceName` template reference), path pattern, and priority are required. The source generator creates standalone CloudFormation resources (TargetGroup, ListenerRule, Lambda Permission) rather than SAM event types.

Expand Down
Loading
Loading