Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bed2729
feat: Add ALBApi attribute definition for Application Load Balancer s…
GarrettBeatty Apr 1, 2026
28590ff
feat: Add ALB source generator model layer
GarrettBeatty Apr 1, 2026
688daca
feat: Add ALB code generation support and dependency validation
GarrettBeatty Apr 1, 2026
a902df1
feat: Add ALB CloudFormation template generation
GarrettBeatty Apr 1, 2026
64a05ba
test: Add ALB CloudFormation writer unit tests
GarrettBeatty Apr 1, 2026
d26a6d3
test: Add ALB source generator snapshot tests
GarrettBeatty Apr 1, 2026
c0cc155
test: Add ALB AWS integration test project
GarrettBeatty Apr 1, 2026
7e84c5a
chore: Add ALB projects to solution and solution filter
GarrettBeatty Apr 1, 2026
6ac3675
fix: replace invalid MultiValueHeadersEnabled with TargetGroupAttribu…
GarrettBeatty Apr 2, 2026
eb0a529
docs: add ALB feature documentation to Annotations README
GarrettBeatty Apr 2, 2026
5184c5c
fix: rename DefaultAction to DefaultActions for ALB Listener
GarrettBeatty Apr 2, 2026
2d57231
Add autover change file for ALB annotations feature
GarrettBeatty Apr 2, 2026
818e53c
Address PR review comments from Copilot
GarrettBeatty Apr 2, 2026
de353af
test: add unit tests for ALB orphan cleanup and metadata tracking
GarrettBeatty Apr 2, 2026
cc1a75a
feat: add compile-time warning for unresolved @reference in ALB Liste…
GarrettBeatty Apr 2, 2026
2deabb2
feat: make ALB @reference not-found diagnostic an error (not warning)
GarrettBeatty Apr 2, 2026
58afd55
Add ALB diagnostic descriptors for FromRoute and unmapped parameters
GarrettBeatty Apr 6, 2026
c9f5316
Update ParameterListExtension to recognize ALB request types as pass-…
GarrettBeatty Apr 6, 2026
9967f34
Update GeneratedMethodModelBuilder to support ALB parameter binding
GarrettBeatty Apr 6, 2026
03f39e9
Create ALBSetupParameters template for ALB parameter binding
GarrettBeatty Apr 6, 2026
38bc77b
Create ALBInvoke template for ALB method invocation and response wrap…
GarrettBeatty Apr 6, 2026
3505bea
Update LambdaFunctionTemplate to dispatch ALB events to ALB templates
GarrettBeatty Apr 6, 2026
025383b
Update LambdaFunctionValidator for ALB flexible parameters and diagno…
GarrettBeatty Apr 6, 2026
3c2c696
Update test app and tests for ALB FromX parameter binding
GarrettBeatty Apr 6, 2026
d4d45db
Add http-header condition support to ALBApiAttribute
GarrettBeatty Apr 6, 2026
b297018
Add query-string and source-ip condition support to ALBApiAttribute
GarrettBeatty Apr 6, 2026
311bcd6
Add ALB-specific FromHeader, FromQuery, FromBody attributes in ALB na…
GarrettBeatty Apr 6, 2026
a01ed96
Fix CloudFormation ALB condition format: use *Config sub-objects
GarrettBeatty Apr 6, 2026
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/alb-annotations-support.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.Annotations",
"Type": "Minor",
"ChangelogMessages": [
"Added [ALBApi] attribute for configuring Lambda functions as targets behind an Application Load Balancer"
]
}
]
}
5 changes: 4 additions & 1 deletion Libraries/Amazon.Lambda.Annotations.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
"test\\TestCustomAuthorizerApp.IntegrationTests\\TestCustomAuthorizerApp.IntegrationTests.csproj",
"test\\TestServerlessApp.IntegrationTests\\TestServerlessApp.IntegrationTests.csproj",
"test\\TestServerlessApp.NET8\\TestServerlessApp.NET8.csproj",
"test\\TestServerlessApp\\TestServerlessApp.csproj"
"src\\Amazon.Lambda.ApplicationLoadBalancerEvents\\Amazon.Lambda.ApplicationLoadBalancerEvents.csproj",
"test\\TestServerlessApp\\TestServerlessApp.csproj",
"test\\TestServerlessApp.ALB\\TestServerlessApp.ALB.csproj",
"test\\TestServerlessApp.ALB.IntegrationTests\\TestServerlessApp.ALB.IntegrationTests.csproj"
]
}
}
30 changes: 30 additions & 0 deletions Libraries/Libraries.sln
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestCustomAuthorizerApp.Int
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestCustomAuthorizerApp", "test\TestCustomAuthorizerApp\TestCustomAuthorizerApp.csproj", "{3BFA4B73-BA61-4578-833B-C5B3A16EDA9E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServerlessApp.ALB", "test\TestServerlessApp.ALB\TestServerlessApp.ALB.csproj", "{8F7C617D-C611-4DC6-A07C-033F13C1835D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServerlessApp.ALB.IntegrationTests", "test\TestServerlessApp.ALB.IntegrationTests\TestServerlessApp.ALB.IntegrationTests.csproj", "{80594C21-C6EB-469E-83CC-68F9F661CA5E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -941,6 +945,30 @@ Global
{3BFA4B73-BA61-4578-833B-C5B3A16EDA9E}.Release|x64.Build.0 = Release|Any CPU
{3BFA4B73-BA61-4578-833B-C5B3A16EDA9E}.Release|x86.ActiveCfg = Release|Any CPU
{3BFA4B73-BA61-4578-833B-C5B3A16EDA9E}.Release|x86.Build.0 = Release|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Debug|x64.ActiveCfg = Debug|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Debug|x64.Build.0 = Debug|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Debug|x86.ActiveCfg = Debug|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Debug|x86.Build.0 = Debug|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Release|Any CPU.Build.0 = Release|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Release|x64.ActiveCfg = Release|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Release|x64.Build.0 = Release|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Release|x86.ActiveCfg = Release|Any CPU
{8F7C617D-C611-4DC6-A07C-033F13C1835D}.Release|x86.Build.0 = Release|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Debug|x64.ActiveCfg = Debug|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Debug|x64.Build.0 = Debug|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Debug|x86.ActiveCfg = Debug|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Debug|x86.Build.0 = Debug|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|Any CPU.Build.0 = Release|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|x64.ActiveCfg = Release|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|x64.Build.0 = Release|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|x86.ActiveCfg = Release|Any CPU
{80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1015,6 +1043,8 @@ Global
{8D03BDF3-7078-4B46-A3F1-C73BE6D6CE0D} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{8EEDD576-7FC4-4FAC-A5A2-F58562753A53} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{3BFA4B73-BA61-4578-833B-C5B3A16EDA9E} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{8F7C617D-C611-4DC6-A07C-033F13C1835D} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{80594C21-C6EB-469E-83CC-68F9F661CA5E} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ AWSLambda0128 | AWSLambdaCSharpGenerator | Error | Authorizer Payload Version Mi
AWSLambda0129 | AWSLambdaCSharpGenerator | Error | Missing LambdaFunction Attribute
AWSLambda0130 | AWSLambdaCSharpGenerator | Error | Invalid return type IAuthorizerResult
AWSLambda0131 | AWSLambdaCSharpGenerator | Error | FromBody not supported on Authorizer functions
AWSLambda0132 | AWSLambdaCSharpGenerator | Error | Invalid ALBApiAttribute
AWSLambda0133 | AWSLambdaCSharpGenerator | Error | ALB Listener Reference Not Found
AWSLambda0134 | AWSLambdaCSharpGenerator | Error | FromRoute not supported on ALB functions
AWSLambda0135 | AWSLambdaCSharpGenerator | Error | Unmapped parameter on ALB function
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,37 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidAlbApiAttribute = new DiagnosticDescriptor(
id: "AWSLambda0132",
title: "Invalid ALBApiAttribute",
messageFormat: "Invalid ALBApiAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor AlbListenerReferenceNotFound = new DiagnosticDescriptor(
id: "AWSLambda0133",
title: "ALB Listener Reference Not Found",
messageFormat: "The ALBApi ListenerArn references '@{0}', but no resource or parameter named '{0}' was found in the CloudFormation template. Add the listener resource to the template or correct the reference name.",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor FromRouteNotSupportedOnAlb = new DiagnosticDescriptor(
id: "AWSLambda0134",
title: "FromRoute not supported on ALB functions",
messageFormat: "[FromRoute] is not supported on ALB functions. ALB does not support route path template parameters. Use [FromHeader], [FromQuery], or [FromBody] instead.",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor AlbUnmappedParameter = new DiagnosticDescriptor(
id: "AWSLambda0135",
title: "Unmapped parameter on ALB function",
messageFormat: "Parameter '{0}' on ALB function has no binding attribute. Use [FromHeader], [FromQuery], [FromBody], or [FromServices], or use the ApplicationLoadBalancerRequest or ILambdaContext types.",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ public static bool HasConvertibleParameter(this IList<ParameterModel> parameters
return false;
}

// ALB request types are forwarded to lambda method if specified, there is no parameter conversion required.
if (TypeFullNames.ALBRequests.Contains(p.Type.FullName))
{
return false;
}

// ILambdaContext is forwarded to lambda method if specified, there is no parameter conversion required.
if (p.Type.FullName == TypeFullNames.ILambdaContext)
{
return false;
}

// Body parameter with target type as string doesn't require conversion because body is string by nature.
if (p.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromBodyAttribute) && p.Type.IsString())
if (p.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromBodyAttribute || att.Type.FullName == TypeFullNames.ALBFromBodyAttribute) && p.Type.IsString())
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Amazon.Lambda.Annotations.ALB;
using Microsoft.CodeAnalysis;
using System;
using System.Linq;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
/// <summary>
/// Builder for <see cref="ALBApiAttribute"/>.
/// </summary>
public class ALBApiAttributeBuilder
{
public static ALBApiAttribute Build(AttributeData att)
{
if (att.ConstructorArguments.Length != 3)
{
throw new NotSupportedException($"{TypeFullNames.ALBApiAttribute} must have constructor with 3 arguments.");
}

var listenerArn = att.ConstructorArguments[0].Value as string;
var pathPattern = att.ConstructorArguments[1].Value as string;
var priority = (int)att.ConstructorArguments[2].Value;

var data = new ALBApiAttribute(listenerArn, pathPattern, priority);

foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.MultiValueHeaders) && pair.Value.Value is bool multiValueHeaders)
{
data.MultiValueHeaders = multiValueHeaders;
}
else if (pair.Key == nameof(data.HostHeader) && pair.Value.Value is string hostHeader)
{
data.HostHeader = hostHeader;
}
else if (pair.Key == nameof(data.HttpMethod) && pair.Value.Value is string httpMethod)
{
data.HttpMethod = httpMethod;
}
else if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName)
{
data.ResourceName = resourceName;
}
else if (pair.Key == nameof(data.HttpHeaderConditionName) && pair.Value.Value is string httpHeaderConditionName)
{
data.HttpHeaderConditionName = httpHeaderConditionName;
}
else if (pair.Key == nameof(data.HttpHeaderConditionValues) && !pair.Value.IsNull)
{
data.HttpHeaderConditionValues = pair.Value.Values.Select(v => v.Value as string).ToArray();
}
else if (pair.Key == nameof(data.QueryStringConditions) && !pair.Value.IsNull)
{
data.QueryStringConditions = pair.Value.Values.Select(v => v.Value as string).ToArray();
}
else if (pair.Key == nameof(data.SourceIpConditions) && !pair.Value.IsNull)
{
data.SourceIpConditions = pair.Value.Values.Select(v => v.Value as string).ToArray();
}
}

return data;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Amazon.Lambda.Annotations.ALB;
using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
/// <summary>
/// Builder for <see cref="ALB.FromHeaderAttribute"/>.
/// </summary>
public class ALBFromHeaderAttributeBuilder
{
public static ALB.FromHeaderAttribute Build(AttributeData att)
{
var data = new ALB.FromHeaderAttribute();
foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.Name) && pair.Value.Value is string value)
{
data.Name = value;
}
}

return data;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Amazon.Lambda.Annotations.ALB;
using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
/// <summary>
/// Builder for <see cref="ALB.FromQueryAttribute"/>.
/// </summary>
public class ALBFromQueryAttributeBuilder
{
public static ALB.FromQueryAttribute Build(AttributeData att)
{
var data = new ALB.FromQueryAttribute();
foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.Name) && pair.Value.Value is string value)
{
data.Name = value;
}
}

return data;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -30,7 +31,7 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.FromQueryAttribute), SymbolEqualityComparer.Default))
{
var data = FromQueryAttributeBuilder.Build(att);
model = new AttributeModel<FromQueryAttribute>
model = new AttributeModel<APIGateway.FromQueryAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
Expand All @@ -39,7 +40,7 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.FromHeaderAttribute), SymbolEqualityComparer.Default))
{
var data = FromHeaderAttributeBuilder.Build(att);
model = new AttributeModel<FromHeaderAttribute>
model = new AttributeModel<APIGateway.FromHeaderAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
Expand Down Expand Up @@ -108,6 +109,42 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ALBApiAttribute), SymbolEqualityComparer.Default))
{
var data = ALBApiAttributeBuilder.Build(att);
model = new AttributeModel<ALBApiAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ALBFromQueryAttribute), SymbolEqualityComparer.Default))
{
var data = ALBFromQueryAttributeBuilder.Build(att);
model = new AttributeModel<ALB.FromQueryAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ALBFromHeaderAttribute), SymbolEqualityComparer.Default))
{
var data = ALBFromHeaderAttributeBuilder.Build(att);
model = new AttributeModel<ALB.FromHeaderAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ALBFromBodyAttribute), SymbolEqualityComparer.Default))
{
var data = new ALB.FromBodyAttribute();
model = new AttributeModel<ALB.FromBodyAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else
{
model = new AttributeModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum EventType
SQS,
DynamoDB,
Schedule,
Authorizer
Authorizer,
ALB
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
{
events.Add(EventType.Authorizer);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.ALBApiAttribute)
{
events.Add(EventType.ALB);
}
}

return events;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ private static TypeModel BuildResponseType(IMethodSymbol lambdaMethodSymbol,
throw new ArgumentOutOfRangeException();
}
}
else if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.ALBApiAttribute))
{
// ALB functions return ApplicationLoadBalancerResponse
// If the user already returns ApplicationLoadBalancerResponse, pass through the return type.
// Otherwise, wrap in ApplicationLoadBalancerResponse.
if (lambdaMethodModel.ReturnsApplicationLoadBalancerResponse)
{
return lambdaMethodModel.ReturnType;
}
var symbol = lambdaMethodModel.ReturnsVoidOrGenericTask ?
task.Construct(context.Compilation.GetTypeByMetadataName(TypeFullNames.ApplicationLoadBalancerResponse)):
context.Compilation.GetTypeByMetadataName(TypeFullNames.ApplicationLoadBalancerResponse);
return TypeModelBuilder.Build(symbol, context);
}
else
{
return lambdaMethodModel.ReturnType;
Expand Down Expand Up @@ -277,6 +291,19 @@ private static IList<ParameterModel> BuildParameters(IMethodSymbol lambdaMethodS
parameters.Add(requestParameter);
parameters.Add(contextParameter);
}
else if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.ALBApiAttribute))
{
var symbol = context.Compilation.GetTypeByMetadataName(TypeFullNames.ApplicationLoadBalancerRequest);
var type = TypeModelBuilder.Build(symbol, context);
var requestParameter = new ParameterModel
{
Name = "__request__",
Type = type,
Documentation = "The ALB request object that will be processed by the Lambda function handler."
};
parameters.Add(requestParameter);
parameters.Add(contextParameter);
}
else
{
// Lambda method with no event attribute are plain lambda functions, therefore, generated method will have
Expand Down
Loading
Loading