FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

usausa/amazon-lambda-extension: Source Generator for AWS Lambda HTTP API · GitHub

Latest commit

 

History

222 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

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

Releases

Packages

Used by

Contributors

Languages


Back | FazBrowse Home | New Git URL