Skip to content

usausa/amazon-lambda-extension

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

193 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AmazonLambdaExtension

English | 日本語

Source Generator for AWS Lambda HTTP API / Event handlers, inspired by Amazon.Lambda.Annotations.

What is this?

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

Features

  • 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 ILambdaFilter with [Filter<T>(Order = N)].
  • AOT compatible — Zero AOT warnings with IsAotCompatible=true and JsonSerializerContext-based JSON serialization.
  • Event handler — Handle non-HTTP events such as SQS with [Event].

Installation

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

Basic Usage

1. Declare a Function class

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

2. Implement the ServiceResolver

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;

3. Configure the Lambda Handler

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"

Parameter Binding

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 returns 400 Invalid request body., and a non-nullable body type returns 400 Request body is required. when the body is null/empty. Declaring the body type as nullable (T?) allows a null body.
  • [FromBody] works without a ServiceResolver. When none is specified, the default JsonBodySerializer.Default and DataAnnotationsRequestValidator implementations 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/Transient lifetimes, 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).
  • ILambdaContext can be added as a parameter anywhere (no binding attribute needed).
  • A [Lambda] class must be a top-level, non-generic, non-abstract partial class (generic types, nested types, records, and abstract are not supported; struct/record struct cannot carry [Lambda] at all). Filters likewise cannot be abstract without a [ServiceResolver].
  • ConfigureServices() of [ServiceResolver] only needs to be a static method reachable from the generated code (inside the Lambda class) — internal within 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).

Filter Pipeline

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 }));
}

Authorizer

[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 an APIGatewayCustomAuthorizerV2Request automatically populates RouteArn.

Event Handler

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);
        }
    }
}

Function URL

[Lambda]
public partial class HealthCheck
{
    [FunctionUrl]
    public IHttpResult Ping()
        => HttpResults.Ok(new { status = "ok", timestamp = DateTime.UtcNow });
}

AOT Compatibility

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 return APIGatewayCustomAuthorizerV2SimpleResponse / APIGatewayCustomAuthorizerV2IamResponse. These are serialized by the Lambda runtime serializer. For AOT, make sure your Lambda serializer's JsonSerializerContext also covers those response types (in addition to your DTOs).

Diagnostics

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)

Scope / Non-goals

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 as AuthType are configured on the SAM / CDK side), and Authorizer = 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.

License

MIT

Packages

 
 
 

Contributors

Languages