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).
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