English | 日本語
Source Generator for AWS Lambda HTTP API / Event handlers, inspired by Amazon.Lambda.Annotations.
AmazonLambdaExtension is a library that uses .NET Source Generators to auto-generate boilerplate code for AWS Lambda functions. It lets you declaratively describe parameter binding, the filter pipeline, and DI integration for HTTP API (API Gateway v2).
- Dependency Injection — Just declare a DI container with the
[ServiceResolver]attribute and constructor arguments are resolved automatically. - Parameter binding —
[FromRoute]/[FromQuery]/[FromHeader]/[FromBody]/[FromServices]/[FromAuthorizer] - HTTP results — Status-code-aware responses via
IHttpResult/HttpResults. - Authorizer — Lambda authorizer generation via
[HttpApiAuthorizer]. - Filter pipeline — Chain multiple classes implementing
ILambdaFilterwith[Filter<T>(Order = N)]. - AOT compatible — Zero AOT warnings with
IsAotCompatible=trueandJsonSerializerContext-based JSON serialization. - Event handler — Handle non-HTTP events such as SQS with
[Event].
<ItemGroup>
<PackageReference Include="AmazonLambdaExtension" Version="x.x.x" />
<PackageReference Include="AmazonLambdaExtension.SourceGenerator" Version="x.x.x">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>[Lambda]
[ServiceResolver(typeof(ServiceResolver))]
public partial class CrudFunctions
{
private readonly DataService data;
public CrudFunctions(DataService data)
{
this.data = data;
}
[HttpApi(LambdaHttpMethod.Get, "/items/{id}")]
public async ValueTask<IHttpResult> GetItem(
[FromRoute] string id,
[FromQuery] int page,
ILambdaContext context)
{
var item = await data.GetAsync(id, page);
return item is null ? HttpResults.NotFound() : HttpResults.Ok(item);
}
[HttpApi(LambdaHttpMethod.Post, "/items")]
public async ValueTask<IHttpResult> CreateItem([FromBody] CreateItemInput input)
{
var created = await data.CreateAsync(input);
return HttpResults.Created($"/items/{created.Id}", created);
}
}An HTTP handler ([HttpApi] / [FunctionUrl]) may return any of the following:
| Return value | Behavior |
|---|---|
IHttpResult / HttpResult (via HttpResults.*) |
Converted to APIGatewayHttpApiV2ProxyResponse |
APIGatewayHttpApiV2ProxyResponse |
Returned as-is |
| Any other type (POCO) | Wrapped into a 200 OK JSON response (equivalent to HttpResults.Ok(value)) |
Task<T> / ValueTask<T> wrappers and synchronous returns are all supported.
The Source Generator calls ServiceResolver.ConfigureServices() to build the DI container.
public static class ServiceResolver
{
public static IServiceCollection ConfigureServices()
{
var services = new ServiceCollection();
// Lambda serializer (AOT-compatible — uses JsonSerializable source generation)
services.AddSingleton<ILambdaSerializer>(
new SourceGeneratorLambdaJsonSerializer<AppJsonContext>());
// Body serializer (AOT-compatible — no reflection by passing a JsonSerializerContext)
services.AddSingleton<IBodySerializer>(new JsonBodySerializer(AppJsonContext.Default));
services.AddSingleton<DataService>();
return services;
}
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(CreateItemInput))]
[JsonSerializable(typeof(Item))]
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))]
internal sealed partial class AppJsonContext : JsonSerializerContext;The handler name generated by the Source Generator is {Namespace}.{ClassName}::{Method}_Handler.
"CrudGet":
Type: AWS::Serverless::Function
Properties:
Handler: "MyApp::MyApp.CrudFunctions::GetItem_Handler"| Attribute | Binds from | HTTP API | Event |
|---|---|---|---|
[FromRoute] |
Path parameter | ✅ | |
[FromQuery] |
Query string | ✅ | |
[FromHeader("name")] |
HTTP header | ✅ | |
[FromBody] |
Request body (JSON) | ✅ | |
[FromServices] / [FromServices("key")] |
DI container | ✅ | ✅ |
[FromAuthorizer("key")] |
Lambda authorizer context | ✅ |
- Optional parameters —
[FromQuery]/[FromRoute]/[FromHeader]parameters with a default value (e.g.[FromQuery] int page = 1,[FromQuery] Mode mode = Mode.Advanced) bind that default when the value is missing, instead of returning 400. Default literals are emitted with invariant culture. - Parameters with
[FromBody]are automatically validated with DataAnnotations; a 400 is returned on failure. Invalid JSON returns400 Invalid request body., and a non-nullable body type returns400 Request body is required.when the body is null/empty. Declaring the body type as nullable (T?) allows a null body. [FromBody]works without aServiceResolver. When none is specified, the defaultJsonBodySerializer.DefaultandDataAnnotationsRequestValidatorimplementations are used.- A
[ServiceResolver]is required when using[FromServices]. [FromServices("key")]resolves a keyed service (GetRequiredKeyedService); without a key it resolves the default service (GetRequiredService).- DI lifecycle: The handler instance itself is a singleton (created once on cold start and reused across the execution environment). For dependencies that need
Scoped/Transientlifetimes, receive them via[FromServices](a method parameter) or filters (resolved from, and disposed with, a per-invocation DI scope). Dependencies injected into the handler's constructor are effectively singletons (to avoid captive dependencies). - The only binding attribute that can be used explicitly in an
[Event]handler is[FromServices]. - An
[Event]handler must declare exactly one payload (event body) parameter (zero or multiple is an error). ILambdaContextcan be added as a parameter anywhere (no binding attribute needed).- A
[Lambda]class must be a top-level, non-generic, non-abstractpartial class(generic types, nested types, records, andabstractare not supported;struct/record structcannot carry[Lambda]at all). Filters likewise cannot beabstractwithout a[ServiceResolver]. ConfigureServices()of[ServiceResolver]only needs to be astaticmethod reachable from the generated code (inside the Lambda class) —internalwithin the same assembly is fine. Likewise, the parameterless constructor of the handler/filter (when there is no[ServiceResolver]) is judged by "reachability."- Handler names must be unique within a class (overloads with the same name are not supported).
Apply classes implementing ILambdaFilter to a class with [Filter<T>(Order = N)].
They are chained in ascending Order, and you can write logic before and after await next(ctx).
public sealed class LoggingFilter : ILambdaFilter
{
public async ValueTask InvokeAsync(LambdaInvocationContext context, LambdaFilterDelegate next)
{
var sw = Stopwatch.StartNew();
await next(context);
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds}ms");
}
}
public sealed class ApiKeyFilter : ILambdaFilter
{
public ValueTask InvokeAsync(LambdaInvocationContext context, LambdaFilterDelegate next)
{
var req = context.GetRequest<APIGatewayHttpApiV2ProxyRequest>();
if (!req.Headers.TryGetValue("x-api-key", out var key) || key != "expected")
{
context.Result = HttpResults.Unauthorized();
return default;
}
return next(context);
}
}
[Lambda]
[ServiceResolver(typeof(ServiceResolver))]
[Filter<LoggingFilter>(Order = 0)]
[Filter<ApiKeyFilter>(Order = 10)]
public partial class SecureFunctions
{
[HttpApi(LambdaHttpMethod.Get, "/secure/items/{id}")]
public ValueTask<HttpResult> GetItem([FromRoute] string id)
=> ValueTask.FromResult(HttpResults.Ok(new { id }));
}[Lambda]
[ServiceResolver(typeof(ServiceResolver))]
public partial class CrudFunctions
{
[HttpApi(LambdaHttpMethod.Post, "/items", Authorizer = nameof(Authorize))]
public async ValueTask<IHttpResult> CreateItem(
[FromBody] CreateItemInput input,
[FromAuthorizer("role")] string role)
{
if (role != "admin") return HttpResults.Forbid();
var created = await data.CreateAsync(input);
return HttpResults.Created($"/items/{created.Id}", created);
}
[HttpApiAuthorizer(EnableSimpleResponses = true)]
public async ValueTask<IAuthorizerResult> Authorize(
APIGatewayHttpApiV2ProxyRequest request,
ILambdaContext context)
{
if (!request.Headers.TryGetValue("authorization", out var token))
return AuthorizerResults.Deny();
return AuthorizerResults.Allow()
.WithPrincipalId("user-123")
.WithContext("role", "admin");
}
}- When using
Authorizer = nameof(Authorize), the target must be a[HttpApiAuthorizer]method in the same class. - When returning the IAM policy format with
EnableSimpleResponses = false, receiving anAPIGatewayCustomAuthorizerV2Requestautomatically populatesRouteArn.
Use [Event] for non-HTTP events (such as SQS).
[Lambda]
[ServiceResolver(typeof(ServiceResolver))]
public partial class QueueProcessor
{
private readonly IProcessor processor;
public QueueProcessor(IProcessor processor)
{
this.processor = processor;
}
[Event]
public async ValueTask Handle(SQSEvent ev, ILambdaContext context)
{
foreach (var record in ev.Records)
{
await processor.HandleAsync(record.Body);
}
}
}[Lambda]
public partial class HealthCheck
{
[FunctionUrl]
public IHttpResult Ping()
=> HttpResults.Ok(new { status = "ok", timestamp = DateTime.UtcNow });
}JsonBodySerializer has two constructors.
| Constructor | AOT support | Use |
|---|---|---|
JsonBodySerializer(JsonSerializerContext) |
✅ | Pass a context generated by [JsonSerializable] (recommended) |
JsonBodySerializer(JsonSerializerOptions) |
❌ | Uses reflection. Already marked with [RequiresDynamicCode] |
For AOT, just use the JsonSerializerContext constructor in your ServiceResolver and declare [JsonSerializable(typeof(T))] (see the ServiceResolver sample above).
HTTP handlers return
APIGatewayHttpApiV2ProxyResponse, and authorizer handlers returnAPIGatewayCustomAuthorizerV2SimpleResponse/APIGatewayCustomAuthorizerV2IamResponse. These are serialized by the Lambda runtime serializer. For AOT, make sure your Lambda serializer'sJsonSerializerContextalso covers those response types (in addition to your DTOs).
| ID | Severity | Phase | Description |
|---|---|---|---|
ALE0001 |
Error | Class structure | [Lambda] class is not partial |
ALE0002 |
Error | Class structure | [Lambda] class is generic |
ALE0003 |
Error | Class structure | [Lambda] class is a nested type |
ALE0004 |
Error | Class structure | [Lambda] applied to a record (record class) — not supported |
ALE0005 |
Error | Class structure | [Lambda] class is abstract |
ALE0006 |
Error | DI/generation | ServiceResolver has no ConfigureServices() method |
ALE0007 |
Error | DI/generation | Constructor parameters present without [ServiceResolver] |
ALE0008 |
Error | DI/generation | [Lambda] class has no parameterless constructor without [ServiceResolver] |
ALE0009 |
Error | Filter | Filter type does not implement ILambdaFilter |
ALE0010 |
Error | Filter | Filter is abstract without [ServiceResolver] |
ALE0011 |
Error | Filter | Filter has no reachable parameterless constructor without [ServiceResolver] |
ALE0012 |
Warning | Handler/parameter | Method without a handler attribute detected |
ALE0013 |
Error | Handler/parameter | Duplicate handler attributes |
ALE0014 |
Warning | Handler/parameter | Target of Authorizer = nameof(...) not found |
ALE0015 |
Error | Handler/parameter | Duplicate binding attributes |
ALE0016 |
Error | Handler/parameter | [FromBody] applied to an [Event] handler |
ALE0017 |
Error | Handler/parameter | Unsupported binding attribute used in an [Event] handler |
ALE0018 |
Warning | Handler/parameter | Misuse of [FromAuthorizer] |
ALE0019 |
Error | Handler/parameter | Type not supported by binding |
ALE0020 |
Error | Handler/parameter | [Event] handler has no payload parameter |
ALE0021 |
Error | Handler/parameter | [Event] handler has multiple payload parameters |
ALE0022 |
Error | Handler/parameter | Invalid return type for [HttpApiAuthorizer] |
ALE0023 |
Error | Post-collection | [FromServices] used without [ServiceResolver] |
ALE0024 |
Error | Post-collection | Overloaded handler name (handler names must be unique) |
This library only generates wrapper code (Source Generator output). The following are intentionally out of scope.
- API Gateway HTTP API (V2) only — REST API (V1) /
[RestApi]/[RestApiAuthorizer], and the HTTP API V1 payload are not supported. - No config file generation — It does not generate or sync
serverless.template/ CloudFormation / SAM.[FunctionUrl]is a marker attribute only (auth/CORS settings such asAuthTypeare configured on the SAM / CDK side), andAuthorizer = nameof(...)is used only for diagnostics, not for emitting infrastructure definitions. - No method-level infrastructure settings — Settings such as
Timeout/MemorySize/Role/Policies/PackageType(equivalent to[LambdaFunction]) should be configured on the SAM / CDK side.
MIT