# AWS Lambda for .NET AWS Lambda for .NET is a comprehensive toolkit that enables .NET developers to build, deploy, and run serverless applications on AWS Lambda. The repository provides NuGet packages for handling various AWS event types (S3, SQS, DynamoDB, API Gateway, etc.), serialization libraries, the Lambda Annotations framework for idiomatic .NET development with attributes and dependency injection, and ASP.NET Core integration for running web applications as Lambda functions. The toolkit supports .NET 6, .NET 8, and .NET 10, with both managed Lambda runtimes and custom runtime/container image deployment options. The core functionality revolves around three main components: event packages that provide strongly-typed classes for AWS service events, the Lambda Annotations framework that uses C# source generators to reduce boilerplate code and automatically sync CloudFormation templates, and the ASP.NET Core Server packages that allow running existing web applications serverlessly. Additionally, the repository includes project templates, a local testing tool for development, and runtime support for custom Lambda runtimes and container images. --- ## Amazon.Lambda.Core - Lambda Context and Logging The `Amazon.Lambda.Core` package provides essential interfaces for Lambda function execution context, logging, and serialization. The `ILambdaContext` interface gives access to function metadata, execution time remaining, and CloudWatch logging. ```csharp using Amazon.Lambda.Core; // Assembly attribute for JSON serialization [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] public class Function { public string FunctionHandler(string input, ILambdaContext context) { // Access function metadata context.Logger.LogLine($"Function name: {context.FunctionName}"); context.Logger.LogLine($"Memory limit: {context.MemoryLimitInMB} MB"); context.Logger.LogLine($"Time remaining: {context.RemainingTime}"); context.Logger.LogLine($"Request ID: {context.AwsRequestId}"); context.Logger.LogLine($"Log stream: {context.LogStreamName}"); return input.ToUpper(); } } ``` --- ## Amazon.Lambda.Annotations - Declarative Lambda Functions The Lambda Annotations framework uses C# attributes and source generators to create Lambda functions with minimal boilerplate. It supports dependency injection, automatic CloudFormation template synchronization, and simplified HTTP parameter binding. ```csharp using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Core; // Startup class for dependency injection [LambdaStartup] public class Startup { public HostApplicationBuilder ConfigureHostBuilder() { var hostBuilder = new HostApplicationBuilder(); hostBuilder.Services.AddSingleton(); hostBuilder.Services.AddAWSService(); return hostBuilder; } } public class Functions { private readonly ICalculatorService _calculator; public Functions(ICalculatorService calculator) { _calculator = calculator; } // HTTP API endpoint with automatic parameter binding [LambdaFunction(MemorySize = 512, Timeout = 30)] [HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")] public int Add(int x, int y, ILambdaContext context) { context.Logger.LogInformation($"Adding {x} + {y}"); return _calculator.Add(x, y); } // REST API with custom role reference [LambdaFunction(Role = "@LambdaExecutionRole")] [RestApi(LambdaHttpMethod.Post, "/process")] public async Task ProcessData( [FromServices] ITracker tracker, [FromBody] DataRequest request, [FromHeader(Name = "X-Request-Id")] string requestId) { tracker.Record(requestId); return HttpResults.Ok(new { status = "processed", id = requestId }); } // Custom HTTP response with headers [LambdaFunction] [HttpApi(LambdaHttpMethod.Get, "/resource/{id}")] public IHttpResult GetResource(int id) { if (id <= 0) return HttpResults.NotFound($"Resource {id} not found") .AddHeader("X-Error-Code", "RESOURCE_NOT_FOUND"); return HttpResults.Ok(new { id, name = "Sample Resource" }); } } ``` --- ## Amazon.Lambda.Annotations - SQS Event Source Mapping Configure Lambda functions to process SQS messages with automatic event source mapping and batch failure reporting. ```csharp using Amazon.Lambda.Annotations; using Amazon.Lambda.Core; using Amazon.Lambda.SQSEvents; public class SQSFunctions { [LambdaFunction(ResourceName = "OrderProcessor", Policies = "AWSLambdaSQSQueueExecutionRole")] [SQSEvent("@OrderQueue", BatchSize = 10, MaximumConcurrency = 5, MaximumBatchingWindowInSeconds = 5, Filters = "{ \"body\" : { \"type\" : [ \"ORDER\" ] } }")] public SQSBatchResponse ProcessOrders(SQSEvent sqsEvent, ILambdaContext context) { var batchItemFailures = new List(); foreach (var record in sqsEvent.Records) { try { context.Logger.LogLine($"Processing message: {record.MessageId}"); // Process the message var order = JsonSerializer.Deserialize(record.Body); ProcessOrder(order); } catch (Exception ex) { context.Logger.LogLine($"Failed to process {record.MessageId}: {ex.Message}"); batchItemFailures.Add(new SQSBatchResponse.BatchItemFailure { ItemIdentifier = record.MessageId }); } } return new SQSBatchResponse { BatchItemFailures = batchItemFailures }; } } ``` --- ## Amazon.Lambda.Annotations - Custom Lambda Authorizers Define HTTP API and REST API authorizers using attributes with automatic CloudFormation integration. ```csharp using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; public class AuthFunctions { // HTTP API Authorizer with simple response [LambdaFunction(ResourceName = "TokenAuthorizer")] [HttpApiAuthorizer(IdentityHeader = "Authorization", ResultTtlInSeconds = 300)] public APIGatewayCustomAuthorizerV2SimpleResponse AuthorizeHttpApi( APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) { var token = request.Headers?.GetValueOrDefault("authorization", ""); if (ValidateToken(token, out var userId, out var email)) { return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = true, Context = new Dictionary { { "userId", userId }, { "email", email } } }; } return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = false }; } // Protected endpoint using the authorizer [LambdaFunction(ResourceName = "ProtectedApi")] [HttpApi(LambdaHttpMethod.Get, "/api/user/profile", Authorizer = nameof(AuthorizeHttpApi))] public object GetUserProfile( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email) { return new { UserId = userId, Email = email, Profile = "User Profile Data" }; } // Simplified authorizer using IAuthorizerResult [LambdaFunction] [HttpApiAuthorizer(EnableSimpleResponses = true)] public IAuthorizerResult SimpleAuthorize( [FromHeader(Name = "X-Api-Key")] string apiKey, ILambdaContext context) { if (string.IsNullOrEmpty(apiKey)) return AuthorizerResults.Deny(); if (apiKey.StartsWith("valid-")) { return AuthorizerResults.Allow() .WithContext("clientId", "client-123") .WithContext("tier", "premium"); } return AuthorizerResults.Deny(); } } ``` --- ## Amazon.Lambda.AspNetCoreServer - ASP.NET Core as Lambda Run ASP.NET Core Web API applications as Lambda functions with API Gateway or Application Load Balancer integration. ```csharp // LambdaEntryPoint.cs - Entry point for Lambda using Amazon.Lambda.AspNetCoreServer; using Microsoft.AspNetCore.Hosting; public class LambdaEntryPoint : APIGatewayProxyFunction { protected override void Init(IWebHostBuilder builder) { builder.UseStartup(); } // Custom path base for ALB routing protected override void PostMarshallRequestFeature( IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) { aspNetCoreRequestFeature.PathBase = "/api/"; aspNetCoreRequestFeature.Path = aspNetCoreRequestFeature.Path.Substring(aspNetCoreRequestFeature.PathBase.Length - 1); } } // Startup.cs - Shared configuration public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddAWSService(); } public void Configure(IApplicationBuilder app) { app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => endpoints.MapControllers()); } } // Controller accessing Lambda context [ApiController] [Route("[controller]")] public class ValuesController : ControllerBase { [HttpGet] public IActionResult Get() { var lambdaContext = (ILambdaContext)Request.HttpContext.Items[ APIGatewayProxyFunction.LAMBDA_CONTEXT]; return Ok(new { RequestId = lambdaContext.AwsRequestId, FunctionName = lambdaContext.FunctionName, RemainingTime = lambdaContext.RemainingTime }); } } ``` --- ## Amazon.Lambda.AspNetCoreServer.Hosting - Minimal API Integration Integrate Lambda hosting into ASP.NET Core minimal API applications with a single line of code. ```csharp using Amazon.Lambda.AspNetCoreServer.Hosting; using Amazon.Lambda.APIGatewayEvents; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Add Lambda hosting - replaces Kestrel when running in Lambda builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, options => { // Register custom binary content types options.RegisterResponseContentEncodingForContentType( "application/x-protobuf", ResponseContentEncoding.Base64); // Enable exception details in development options.IncludeUnhandledExceptionDetailInResponse = builder.Environment.IsDevelopment(); // Customize request marshalling options.PostMarshallRequestFeature = (requestFeature, lambdaRequest, context) => { var apiRequest = (APIGatewayHttpApiV2ProxyRequest)lambdaRequest; requestFeature.Headers["X-Stage"] = apiRequest.RequestContext.Stage; }; // Add Lambda context to HttpContext.Items options.PostMarshallItemsFeature = (itemsFeature, lambdaRequest, context) => { itemsFeature.Items["LambdaRequestId"] = context.AwsRequestId; }; }); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); app.UseAuthorization(); app.MapControllers(); // Minimal API endpoints app.MapGet("/hello/{name}", (string name) => $"Hello, {name}!"); app.MapPost("/echo", (object data) => Results.Ok(data)); app.Run(); ``` --- ## Amazon.Lambda.S3Events - S3 Event Processing Handle S3 bucket notifications for object creation, deletion, and other events. ```csharp using Amazon.Lambda.Core; using Amazon.Lambda.S3Events; using Amazon.S3; using Amazon.S3.Model; [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] public class S3Function { private readonly IAmazonS3 _s3Client; public S3Function() { _s3Client = new AmazonS3Client(); } public async Task FunctionHandler(S3Event s3Event, ILambdaContext context) { foreach (var record in s3Event.Records) { var bucket = record.S3.Bucket.Name; var key = record.S3.Object.Key; var eventName = record.EventName; var eventTime = record.EventTime; context.Logger.LogLine($"[{eventName}] {bucket}/{key} at {eventTime}"); if (eventName.Value.StartsWith("ObjectCreated")) { // Get object metadata var response = await _s3Client.GetObjectMetadataAsync(bucket, key); context.Logger.LogLine($"Content-Type: {response.Headers.ContentType}"); context.Logger.LogLine($"Size: {response.Headers.ContentLength} bytes"); // Process the object using var objectResponse = await _s3Client.GetObjectAsync(bucket, key); await ProcessS3Object(objectResponse.ResponseStream, context); } } } private async Task ProcessS3Object(Stream stream, ILambdaContext context) { // Process the S3 object stream context.Logger.LogLine("Processing S3 object..."); } } ``` --- ## Amazon.Lambda.DynamoDBEvents - DynamoDB Streams Processing Process DynamoDB Streams events with batch failure reporting and time window aggregation support. ```csharp using Amazon.Lambda.Core; using Amazon.Lambda.DynamoDBEvents; [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] public class DynamoDBFunction { // Basic stream processing with batch failure reporting public StreamsEventResponse ProcessRecords(DynamoDBEvent dynamoEvent, ILambdaContext context) { var batchItemFailures = new List(); foreach (var record in dynamoEvent.Records) { try { var ddbRecord = record.Dynamodb; var eventType = record.EventName; var keys = string.Join(", ", ddbRecord.Keys.Keys); context.Logger.LogLine($"Event: {eventType}, Keys: [{keys}], Size: {ddbRecord.SizeBytes}"); if (eventType == OperationType.INSERT) { var newImage = ddbRecord.NewImage; // Process new item } else if (eventType == OperationType.MODIFY) { var oldImage = ddbRecord.OldImage; var newImage = ddbRecord.NewImage; // Process modification } } catch (Exception ex) { context.Logger.LogLine($"Error processing {record.Dynamodb.SequenceNumber}: {ex.Message}"); batchItemFailures.Add(new StreamsEventResponse.BatchItemFailure { ItemIdentifier = record.Dynamodb.SequenceNumber }); } } return new StreamsEventResponse { BatchItemFailures = batchItemFailures }; } // Time window aggregation public DynamoDBTimeWindowResponse AggregateWithTimeWindow( DynamoDBTimeWindowEvent timeWindowEvent, ILambdaContext context) { context.Logger.LogLine($"Shard: {timeWindowEvent.ShardId}, Window: {timeWindowEvent.Window}"); var state = timeWindowEvent.State ?? new Dictionary(); if (timeWindowEvent.IsFinalInvokeForWindow == true) { context.Logger.LogLine("Final invoke - processing aggregated state"); // Process final aggregated state } else { // Aggregate records foreach (var record in timeWindowEvent.Records) { var id = record.Dynamodb.NewImage["Id"].N; state[id] = state.ContainsKey(id) ? (int.Parse(state[id]) + 1).ToString() : "1"; } } return new DynamoDBTimeWindowResponse { State = state }; } } ``` --- ## Amazon.Lambda.APIGatewayEvents - API Gateway Integration Handle API Gateway REST API, HTTP API, and WebSocket API events directly without ASP.NET Core. ```csharp using Amazon.Lambda.Core; using Amazon.Lambda.APIGatewayEvents; [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] public class ApiGatewayFunction { // REST API / HTTP API v1 handler public APIGatewayProxyResponse HandleRestApi( APIGatewayProxyRequest request, ILambdaContext context) { context.Logger.LogLine($"Request: {request.RequestContext.RequestId}"); context.Logger.LogLine($"Method: {request.HttpMethod}, Path: {request.Path}"); context.Logger.LogLine($"Headers: {string.Join(", ", request.Headers.Keys)}"); // Access path and query parameters var id = request.PathParameters?["id"]; var filter = request.QueryStringParameters?["filter"]; return new APIGatewayProxyResponse { StatusCode = 200, Headers = new Dictionary { { "Content-Type", "application/json" }, { "X-Request-Id", request.RequestContext.RequestId } }, Body = JsonSerializer.Serialize(new { id, filter, message = "Success" }) }; } // HTTP API v2 handler public APIGatewayHttpApiV2ProxyResponse HandleHttpApiV2( APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) { context.Logger.LogLine($"Route: {request.RouteKey}"); context.Logger.LogLine($"Stage: {request.RequestContext.Stage}"); // Parse request body var body = request.IsBase64Encoded ? Encoding.UTF8.GetString(Convert.FromBase64String(request.Body)) : request.Body; return new APIGatewayHttpApiV2ProxyResponse { StatusCode = 200, Headers = new Dictionary { { "Content-Type", "application/json" } }, Body = JsonSerializer.Serialize(new { received = body }) }; } } ``` --- ## Amazon.Lambda.RuntimeSupport - Custom Runtime and Container Images Build Lambda functions with custom runtimes or container images using the Lambda Runtime Interface Client. ```csharp using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; using Amazon.Lambda.Serialization.SystemTextJson; // Option 1: Top-level statements with LambdaBootstrapBuilder var handler = (string input, ILambdaContext context) => { context.Logger.LogLine($"Processing: {input}"); return input.ToUpper(); }; await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer()) .Build() .RunAsync(); // Option 2: Using HandlerWrapper for existing handlers public class Program { private static async Task Main(string[] args) { using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( (Func)Handler, new DefaultLambdaJsonSerializer()); using var bootstrap = new LambdaBootstrap(handlerWrapper); await bootstrap.RunAsync(); } public static Response Handler(Request request, ILambdaContext context) { return new Response { Result = $"Processed: {request.Data}" }; } } // Option 3: Direct InvocationRequest handling for advanced scenarios public class AdvancedFunction { private static readonly JsonSerializer Serializer = new JsonSerializer(); private static readonly MemoryStream ResponseStream = new MemoryStream(); private static async Task Main(string[] args) { using var bootstrap = new LambdaBootstrap(ProcessAsync); await bootstrap.RunAsync(); } private static Task ProcessAsync(InvocationRequest invocation) { var input = Serializer.Deserialize(invocation.InputStream); var output = new Output { Result = input.Value * 2 }; ResponseStream.SetLength(0); Serializer.Serialize(output, ResponseStream); ResponseStream.Position = 0; return Task.FromResult(new InvocationResponse(ResponseStream, false)); } } ``` --- ## Amazon.Lambda.Serialization.SystemTextJson - JSON Serialization Configure JSON serialization with System.Text.Json, including source generator support for improved performance. ```csharp using System.Text.Json; using System.Text.Json.Serialization; using Amazon.Lambda.Core; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Serialization.SystemTextJson; // Option 1: Default serializer (assembly-level) [assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))] // Option 2: Source generator for better cold start performance [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] [JsonSerializable(typeof(MyCustomRequest))] [JsonSerializable(typeof(MyCustomResponse))] public partial class LambdaJsonSerializerContext : JsonSerializerContext { } [assembly: LambdaSerializer(typeof(SourceGeneratorLambdaJsonSerializer))] // Option 3: Custom serializer with options public class CustomLambdaSerializer : DefaultLambdaJsonSerializer { public CustomLambdaSerializer() : base(CreateCustomizer()) { } private static Action CreateCustomizer() { return options => { options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; options.Converters.Add(new JsonStringEnumConverter()); }; } } // Usage with custom serializer on method public class Function { [LambdaSerializer(typeof(CustomLambdaSerializer))] public MyCustomResponse Handler(MyCustomRequest request, ILambdaContext context) { return new MyCustomResponse { Status = "OK", Data = request.Input.ToUpper() }; } } ``` --- ## Amazon.Lambda.TestUtilities - Local Testing Unit test Lambda functions locally with mock implementations of Lambda interfaces. ```csharp using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using Xunit; public class FunctionTests { [Fact] public void TestBasicHandler() { var function = new Function(); var context = new TestLambdaContext { FunctionName = "TestFunction", MemoryLimitInMB = 256, RemainingTime = TimeSpan.FromMinutes(5) }; var result = function.FunctionHandler("hello world", context); Assert.Equal("HELLO WORLD", result); } [Fact] public async Task TestApiGatewayHandler() { var function = new ApiFunction(); var context = new TestLambdaContext(); var request = new APIGatewayProxyRequest { HttpMethod = "GET", Path = "/api/users/123", PathParameters = new Dictionary { { "id", "123" } }, Headers = new Dictionary { { "Authorization", "Bearer token" } } }; var response = await function.GetUser(request, context); Assert.Equal(200, response.StatusCode); Assert.Contains("123", response.Body); } [Fact] public void TestS3EventHandler() { var function = new S3Function(); var context = new TestLambdaContext(); var s3Event = new S3Event { Records = new List { new S3Event.S3EventNotificationRecord { EventName = "ObjectCreated:Put", S3 = new S3Event.S3Entity { Bucket = new S3Event.S3BucketEntity { Name = "test-bucket" }, Object = new S3Event.S3ObjectEntity { Key = "test-key.txt" } } } } }; function.FunctionHandler(s3Event, context); // Verify expected behavior } } ``` --- ## AWS Lambda Test Tool - Local Development and Testing Install and use the AWS Lambda Test Tool for local testing with Lambda and API Gateway emulation. ```bash # Install the test tool dotnet tool install -g amazon.lambda.testtool # Update to latest version dotnet tool update -g amazon.lambda.testtool # Start Lambda emulator only dotnet lambda-test-tool start --lambda-emulator-port 5050 # Start API Gateway emulator with REST mode dotnet lambda-test-tool start \ --api-gateway-emulator-port 5051 \ --api-gateway-emulator-mode Rest # Start both emulators (combined mode) dotnet lambda-test-tool start \ --lambda-emulator-port 5050 \ --api-gateway-emulator-port 5051 \ --api-gateway-emulator-mode HttpV2 # Configure API Gateway routes (environment variable) export APIGATEWAY_EMULATOR_ROUTE_CONFIG='[ {"LambdaResourceName":"AddFunction","HttpMethod":"GET","Path":"/add/{x}/{y}"}, {"LambdaResourceName":"SubtractFunction","HttpMethod":"GET","Path":"/subtract/{x}/{y}"} ]' # Test the function with curl curl -X GET "http://localhost:5051/add/5/3" # Expected: 8 ``` --- ## Lambda Project Templates - Creating New Projects Use dotnet CLI templates to create new Lambda projects with pre-configured settings. ```bash # Install Lambda templates dotnet new install Amazon.Lambda.Templates # List available Lambda templates dotnet new list --author AWS # Create an empty Lambda function dotnet new lambda.EmptyFunction --name MyFunction --profile default --region us-east-1 # Create Lambda Annotations project dotnet new serverless.Annotations --name MyAnnotationsApi # Create ASP.NET Core Web API for Lambda dotnet new serverless.AspNetCoreWebAPI --name MyWebApi # Create Minimal API for Lambda dotnet new serverless.AspNetCoreMinimalAPI --name MyMinimalApi # Create Native AOT Lambda function dotnet new lambda.NativeAOT --name MyAotFunction # Create SQS-triggered Lambda dotnet new lambda.SQS --name MySqsProcessor # Create DynamoDB-triggered Lambda dotnet new lambda.DynamoDB --name MyDynamoProcessor # Deploy using Amazon.Lambda.Tools dotnet tool install -g Amazon.Lambda.Tools dotnet lambda deploy-function MyFunction dotnet lambda deploy-serverless --stack-name MyStack ``` --- ## Summary AWS Lambda for .NET provides a comprehensive ecosystem for building serverless applications. The primary use cases include: (1) building REST/HTTP APIs with Lambda Annotations or ASP.NET Core for automatic parameter binding, dependency injection, and CloudFormation synchronization; (2) processing event-driven workloads from S3, SQS, DynamoDB Streams, SNS, and other AWS services with strongly-typed event classes; (3) running existing ASP.NET Core web applications serverlessly with minimal code changes; and (4) creating custom runtimes or container-based Lambda functions for scenarios requiring specific .NET versions or native dependencies. Integration patterns typically involve combining the Lambda Annotations framework with dependency injection for API-driven microservices, using the event packages directly for background processing tasks, or leveraging ASP.NET Core Server for migrating existing web applications. The toolkit supports both SAM (Serverless Application Model) and raw CloudFormation deployments, with source generators automatically maintaining CloudFormation templates. For local development, the Lambda Test Tool provides both Lambda emulation and API Gateway emulation, enabling end-to-end testing without deploying to AWS. The serialization packages optimize cold start performance through System.Text.Json source generators, making the toolkit suitable for latency-sensitive applications.