Skip to content

Add [ScheduleEvent] annotation attribute and source generator support#2323

Draft
GarrettBeatty wants to merge 1 commit intodevfrom
feature/schedule-annotations
Draft

Add [ScheduleEvent] annotation attribute and source generator support#2323
GarrettBeatty wants to merge 1 commit intodevfrom
feature/schedule-annotations

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Contributor

@GarrettBeatty GarrettBeatty commented Apr 3, 2026

Summary

Adds [ScheduleEvent] annotation attribute support to the Lambda Annotations framework, enabling developers to declaratively configure schedule-triggered Lambda functions directly in C# code using rate or cron expressions. The source generator automatically produces the corresponding SAM/CloudFormation template configuration at build time.

User Experience

With this change, developers can write schedule-triggered Lambda functions like this:

[LambdaFunction]
[ScheduleEvent("rate(5 minutes)")]
public async Task ProcessScheduledEvent(ScheduledEvent evnt)
{
    // Handle scheduled invocation
}

The source generator will automatically generate the SAM template entry:

ProcessScheduledEvent:
  Type: AWS::Serverless::Function
  Properties:
    Events:
      rate5minutes:
        Type: Schedule
        Properties:
          Schedule: rate(5 minutes)

Attribute Properties

Property Required Description Default
Schedule Yes Schedule expression (rate(...) or cron(...)) -
ResourceName No CloudFormation event resource name Derived from schedule expression
Description No Description for the schedule rule Not set
Input No JSON string to pass as input to the Lambda function Not set
Enabled No Whether the event source is enabled true

Compile-Time Validation

The source generator validates at build time:

  • Schedule expression: Must start with rate( or cron(
  • Method signature: First parameter must be ScheduledEvent, optional second parameter must be ILambdaContext
  • Return type: Must be void or Task
  • Dependencies: Project must reference Amazon.Lambda.CloudWatchEvents NuGet package
  • Resource name: Must be alphanumeric if explicitly set

Example with all properties

[LambdaFunction]
[ScheduleEvent("cron(0 12 * * ? *)",
    ResourceName = "DailyCleanup",
    Description = "Runs daily at noon UTC",
    Input = "{\"action\": \"cleanup\"}",
    Enabled = true)]
public async Task ProcessScheduledEvent(ScheduledEvent evnt, ILambdaContext context)
{
    context.Logger.LogLine("Running scheduled cleanup");
}

What Changed

Annotation Attribute (Amazon.Lambda.Annotations)

  • New ScheduleEventAttribute class in Amazon.Lambda.Annotations.Schedule namespace with configurable properties and built-in validation

Source Generator (Amazon.Lambda.Annotations.SourceGenerator)

  • ScheduleEventAttributeBuilder — extracts attribute data from Roslyn syntax tree
  • AttributeModelBuilder — recognizes and routes ScheduleEvent attributes
  • EventTypeBuilder — maps to EventType.Schedule
  • SyntaxReceiver — registers ScheduleEvent as a recognized attribute
  • TypeFullNames — adds Schedule type constants
  • LambdaFunctionValidator — validates method signatures, return types, dependencies, and attribute properties
  • CloudFormationWriter.ProcessScheduleAttribute() — generates SAM template with Schedule expression, Description, Input, and Enabled
  • New diagnostic AWSLambda0132 for invalid ScheduleEventAttribute errors

Tests

  • Attribute unit tests covering constructor, defaults, property tracking, and validation
  • CloudFormation writer tests covering attribute configurations and template formats
  • E2E source generator snapshot tests (sync + async)
  • Integration test deploying a real schedule-triggered Lambda and verifying EventBridge rule

Related: DOTNET-8574

@GarrettBeatty GarrettBeatty force-pushed the feature/schedule-annotations branch 2 times, most recently from 4dd8798 to b1c1d26 Compare April 13, 2026 17:31
- ScheduleEventAttribute with Schedule (rate/cron), ResourceName, Description, Input, Enabled
- ScheduleEventAttributeBuilder for Roslyn AttributeData parsing
- Source generator wiring (TypeFullNames, SyntaxReceiver, EventTypeBuilder, AttributeModelBuilder)
- CloudFormationWriter ProcessScheduleAttribute (SAM Schedule event rule)
- LambdaFunctionValidator ValidateScheduleEvents
- DiagnosticDescriptors InvalidScheduleEventAttribute
- ScheduleEventAttributeTests (attribute unit tests)
- ScheduleEventsTests (CloudFormation writer tests)
- E2E source generator snapshot tests
- Integration test (ScheduleEventRule)
- Sample function (ScheduledProcessing)
- .autover change file
- README documentation
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class [ScheduleEvent] support to the Lambda Annotations framework so schedule-triggered Lambdas (rate/cron) can be declared in C# and emitted into the generated SAM/CloudFormation template.

Changes:

  • Introduces ScheduleEventAttribute (schedule expression + optional ResourceName/Description/Input/Enabled).
  • Extends the source generator to recognize/validate schedule events and emit Type: Schedule event configuration in the template.
  • Adds unit, snapshot, and integration tests plus docs/examples for schedule events.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Libraries/test/TestServerlessApp/TestServerlessApp.csproj Adds CloudWatchEvents project reference for schedule event sample app.
Libraries/test/TestServerlessApp/ScheduledProcessing.cs Adds an example scheduled handler Lambda using [ScheduleEvent].
Libraries/test/TestServerlessApp/ScheduleEventExamples/ValidScheduleEvents.cs.txt Adds valid schedule event usages for generator snapshot tests (kept as .txt).
Libraries/test/TestServerlessApp.IntegrationTests/TestServerlessApp.IntegrationTests.csproj Adds AWSSDK.CloudWatchEvents dependency for schedule integration testing.
Libraries/test/TestServerlessApp.IntegrationTests/ScheduleEventRule.cs Adds integration test to validate the deployed EventBridge rule configuration.
Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs Updates expected Lambda function count to include the new scheduled handler.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs Adds CloudFormation writer tests for schedule event emission and property syncing.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs Adds snapshot-based source generator test for valid schedule events.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/scheduleEvents.template Adds expected generated serverless template snapshot for schedule events.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEvent_Generated.g.cs Adds expected generated handler snapshot (sync).
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEventAsync_Generated.g.cs Adds expected generated handler snapshot (async).
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/ScheduleEventAttributeTests.cs Adds unit tests for ScheduleEventAttribute defaults/derivations/validation.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs Adds metadata reference for ScheduledEvent type in the Roslyn test harness.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj Adds CloudWatchEvents project reference for tests.
Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs Introduces the new ScheduleEventAttribute implementation + validation.
Libraries/src/Amazon.Lambda.Annotations/README.md Documents ScheduleEvent usage and updates supported-attributes list.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs Emits Schedule SAM event entries for [ScheduleEvent].
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs Adds dependency checks + signature/return/attribute validation for schedule handlers.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs Adds full-name constants for ScheduleEventAttribute and ScheduledEvent.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs Recognizes ScheduleEventAttribute during syntax collection.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs Maps ScheduleEventAttribute to EventType.Schedule.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/ScheduleEventAttributeBuilder.cs Builds ScheduleEventAttribute models from Roslyn attribute data.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs Routes ScheduleEventAttribute to the new builder/model type.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs Adds a new diagnostic descriptor for invalid ScheduleEventAttribute usage.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md Records the new diagnostic ID in analyzer release notes.
.autover/changes/add-scheduleevent-annotation.json Adds versioning/changelog entry for the new feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +285 to +290
public static readonly DiagnosticDescriptor InvalidScheduleEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0139",
title: "Invalid ScheduleEventAttribute",
messageFormat: "Invalid ScheduleEventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
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.
<ItemGroup>
<!-- AWSSDK.SecurityToken is needed at runtime for environments which uses assume-role operation for credentials -->
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.1.99" />
<PackageReference Include="AWSSDK.CloudWatchEvents" Version="3.7.*" />
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 package reference uses a floating version (3.7.*), while other test projects in this repo pin exact AWSSDK package versions. Floating versions can make CI non-deterministic. Prefer an explicit AWSSDK.CloudWatchEvents version aligned with the other 3.7.x dependencies used by the integration tests.

Suggested change
<PackageReference Include="AWSSDK.CloudWatchEvents" Version="3.7.*" />
<PackageReference Include="AWSSDK.CloudWatchEvents" Version="3.7.1.99" />

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +32
var rulesResponse = await eventsClient.ListRulesAsync(new ListRulesRequest());

// Find the rule targeting our function
var matchingRule = rulesResponse.Rules.FirstOrDefault(r =>
r.Name.Contains("FiveMinuteSchedule") || r.Name.Contains("ScheduledHandler"));

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.

ListRulesAsync is paginated and may not return the matching rule on the first page. Also, listing all rules in the account/region and then searching by substring can create false positives or flaky tests in shared accounts. Consider using pagination and narrowing the query (e.g., NamePrefix based on the stack name, or ListRuleNamesByTarget/ListTargetsByRule to ensure the rule actually targets the deployed function).

Suggested change
var rulesResponse = await eventsClient.ListRulesAsync(new ListRulesRequest());
// Find the rule targeting our function
var matchingRule = rulesResponse.Rules.FirstOrDefault(r =>
r.Name.Contains("FiveMinuteSchedule") || r.Name.Contains("ScheduledHandler"));
Rule matchingRule = null;
string nextToken = null;
do
{
var rulesResponse = await eventsClient.ListRulesAsync(new ListRulesRequest
{
NextToken = nextToken
});
foreach (var rule in rulesResponse.Rules.Where(r =>
string.Equals(r.ScheduleExpression, "rate(5 minutes)") &&
string.Equals(r.Description, "Runs every 5 minutes")))
{
var targetsResponse = await eventsClient.ListTargetsByRuleAsync(new ListTargetsByRuleRequest
{
Rule = rule.Name
});
if (targetsResponse.Targets.Any(t => t.Arn != null && t.Arn.Contains($":function:{lambdaFunctionName}")))
{
matchingRule = rule;
break;
}
}
if (matchingRule != null)
{
break;
}
nextToken = rulesResponse.NextToken;
}
while (!string.IsNullOrEmpty(nextToken));

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +23
var lambdaFunctionName = _fixture.LambdaFunctions.FirstOrDefault(x => string.Equals(x.LogicalId, "ScheduledHandler"))?.Name;
Assert.NotNull(lambdaFunctionName);
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.

lambdaFunctionName is retrieved and asserted non-null, but it’s never used afterward. Either remove it (and just assert the function exists), or use it to scope the rule lookup (for example, by verifying the rule actually targets this function).

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +65
/// <summary>
/// If set to false, the event source mapping will be disabled. Default value is true.
/// </summary>
public bool Enabled
{
get => enabled.GetValueOrDefault();
set => enabled = value;
}
private bool? enabled { get; set; }
internal bool IsEnabledSet => enabled.HasValue;

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.

Enabled is documented as defaulting to true, but the getter currently uses enabled.GetValueOrDefault() which returns false when the property was never set. This can lead to incorrect behavior for any tooling or user code that reads Enabled (and it also contradicts the XML doc). Consider returning enabled.GetValueOrDefault(true) (and keep using IsEnabledSet to decide whether to emit the property in the template).

Copilot uses AI. Check for mistakes.
return resourceName;
}
// Generate a default resource name from the schedule expression
var sanitized = string.Join(string.Empty, Schedule.Where(char.IsLetterOrDigit));
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.

ResourceName derives a default name by enumerating Schedule.Where(...), but Schedule can be null (e.g., new ScheduleEventAttribute(null)), which would throw a NullReferenceException if ResourceName is accessed before validation short-circuits generation. Make the derivation null-safe (e.g., treat null as empty) so invalid attributes fail with diagnostics instead of potentially crashing the generator or user code.

Suggested change
var sanitized = string.Join(string.Empty, Schedule.Where(char.IsLetterOrDigit));
var sanitized = string.Join(string.Empty, (Schedule ?? string.Empty).Where(char.IsLetterOrDigit));

Copilot uses AI. Check for mistakes.
Comment on lines +452 to +462
// 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));
}
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants