From 6b986d068079e558480f851feb1e07d77cb3ddce Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 29 Nov 2025 23:13:25 -0500 Subject: [PATCH 01/65] feat(docs): add core concepts and first lambda function tutorial - Documented core concepts, including lifecycle, DI, middleware, and source generation. - Added a complete tutorial for creating the first Lambda function using aws-lambda-host. - Provided step-by-step examples, complete code, and testing/deployment instructions. --- docs/getting-started/core-concepts.md | 790 ++++++++++++++++++++++ docs/getting-started/first-lambda.md | 602 +++++++++++++++++ docs/getting-started/index.md | 216 ++++++ docs/getting-started/installation.md | 269 ++++++++ docs/getting-started/project-structure.md | 677 ++++++++++++++++++ mkdocs.yml | 14 +- 6 files changed, 2566 insertions(+), 2 deletions(-) create mode 100644 docs/getting-started/core-concepts.md create mode 100644 docs/getting-started/first-lambda.md create mode 100644 docs/getting-started/index.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/project-structure.md diff --git a/docs/getting-started/core-concepts.md b/docs/getting-started/core-concepts.md new file mode 100644 index 00000000..2e3e9da1 --- /dev/null +++ b/docs/getting-started/core-concepts.md @@ -0,0 +1,790 @@ +# Core Concepts + +This guide explores the fundamental concepts that power aws-lambda-host: the Lambda lifecycle, dependency injection, middleware pipelines, handler registration, and source generation. Understanding these concepts will help you build robust, maintainable Lambda functions. + +## Lambda Lifecycle + +AWS Lambda functions go through three distinct phases during their execution. Understanding these phases is crucial for optimizing performance and managing resources effectively. + +### The Three Phases + +#### 1. OnInit Phase (Cold Start) + +The OnInit phase runs **once** when a Lambda container starts (cold start). This is your opportunity to perform expensive setup operations that can be reused across multiple invocations. + +**Characteristics:** +- Runs only once per container lifecycle +- Singleton services are created during this phase +- Exceptions here prevent the Lambda from starting +- Should complete quickly to minimize cold start time + +**Perfect for:** +- Opening database connections +- Loading configuration +- Warming caches +- Initializing HTTP clients +- Setting up telemetry + +**Example:** + +```csharp +lambda.OnInit(async (services, cancellationToken) => +{ + var cache = services.GetRequiredService(); + await cache.WarmupAsync(cancellationToken); + Console.WriteLine("Cache warmed up successfully"); + return true; // Return false to abort startup +}); +``` + +#### 2. Invocation Phase + +The Invocation phase runs **for each Lambda event**. This is where your business logic executes. + +**Characteristics:** +- Runs for every invocation +- New DI scope created per invocation +- Scoped services are instantiated +- Handler executes with injected dependencies +- Exceptions are returned as error responses + +**What happens:** +1. Lambda Runtime receives event +2. Event is deserialized to your model +3. New DI scope created +4. Middleware pipeline executes +5. Handler executes +6. Response serialized to JSON +7. DI scope disposed + +#### 3. OnShutdown Phase + +The OnShutdown phase runs **once** before the Lambda container terminates. Use this for cleanup and flushing data. + +**Characteristics:** +- Runs once before container shutdown +- Limited time window (configurable, default 2 seconds) +- Perfect for flushing metrics, logs, or buffers +- Services are still available for injection + +**Perfect for:** +- Flushing telemetry data +- Closing database connections +- Final cleanup operations +- Logging shutdown events + +**Example:** + +```csharp +lambda.OnShutdown(async (services, cancellationToken) => +{ + var metrics = services.GetRequiredService(); + await metrics.FlushAsync(cancellationToken); + Console.WriteLine("Metrics flushed before shutdown"); +}); +``` + +### Lifecycle Timeline + +```mermaid +graph LR + A[Cold Start] --> B[OnInit Phase] + B --> C{Init Success?} + C -->|Yes| D[Ready] + C -->|No| E[Failed] + D --> F[Invocation 1] + F --> G[Invocation 2] + G --> H[Invocation 3] + H --> I[...] + I --> J[Shutdown Signal] + J --> K[OnShutdown Phase] + K --> L[Terminated] +``` + +### Complete Lifecycle Example + +```csharp +using System; +using System.Threading.Tasks; +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +// Register services +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +var lambda = builder.Build(); + +// OnInit - runs once on cold start +lambda.OnInit(async (services, cancellationToken) => +{ + Console.WriteLine("=== INIT PHASE START ==="); + var cache = services.GetRequiredService(); + await cache.WarmupAsync(cancellationToken); + Console.WriteLine("=== INIT PHASE COMPLETE ==="); + return true; +}); + +// Handler - runs for each invocation +lambda.MapHandler(([Event] Order order) => +{ + Console.WriteLine($"=== INVOCATION: Order {order.Id} ==="); + return new OrderResponse(order.Id, true); +}); + +// OnShutdown - runs once before termination +lambda.OnShutdown(async (services, cancellationToken) => +{ + Console.WriteLine("=== SHUTDOWN PHASE START ==="); + var cache = services.GetRequiredService(); + await cache.FlushAsync(cancellationToken); + Console.WriteLine("=== SHUTDOWN PHASE COMPLETE ==="); +}); + +await lambda.RunAsync(); +``` + +!!! tip "Lifecycle Timeouts" + You can configure timeout durations using `LambdaHostOptions`: + ```csharp + builder.Services.ConfigureLambdaHostOptions(options => + { + options.InitTimeout = TimeSpan.FromSeconds(10); + options.ShutdownDuration = TimeSpan.FromSeconds(5); + }); + ``` + +## Dependency Injection + +Dependency injection (DI) is a core feature of aws-lambda-host. It enables testable, maintainable code by managing dependencies through a service container. + +### Service Lifetimes + +The framework supports two service lifetimes, matching Lambda's execution model: + +#### Singleton (Container Level) + +Singleton services are created **once** during the OnInit phase and **reused across all invocations**. + +**Characteristics:** +- Created during OnInit +- Shared across all invocations +- Must be thread-safe +- Efficient for shared resources + +**Perfect for:** +- HTTP clients (HttpClient, HttpClientFactory) +- Caches and in-memory stores +- Configuration objects +- Database connection pools +- Stateless services + +**Example:** + +```csharp +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(builder.Configuration); +``` + +!!! warning "Thread Safety Required" + Singleton services must be thread-safe because they can be accessed by concurrent invocations. + +#### Scoped (Invocation Level) + +Scoped services are created **once per invocation** and **disposed after the invocation completes**. + +**Characteristics:** +- Created when invocation starts +- Isolated between invocations +- Automatically disposed after invocation +- Can maintain per-request state + +**Perfect for:** +- Database contexts (Entity Framework DbContext) +- Repositories with invocation-specific state +- Unit of Work patterns +- Request-specific loggers +- Transaction managers + +**Example:** + +```csharp +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +### DI Scope Visualization + +```mermaid +graph TD + Container[DI Container
Singleton Services] --> Scope1[Invocation 1 Scope
Scoped Services] + Container --> Scope2[Invocation 2 Scope
Scoped Services] + Container --> Scope3[Invocation 3 Scope
Scoped Services] + + Scope1 -.->|Disposed after| Inv1[Invocation 1 Complete] + Scope2 -.->|Disposed after| Inv2[Invocation 2 Complete] + Scope3 -.->|Disposed after| Inv3[Invocation 3 Complete] +``` + +### Injectable Parameters + +Handlers can inject various types of parameters: + +| Parameter Type | Description | Attribute Required | +|---------------|-------------|-------------------| +| Event Model | The Lambda event (your custom type) | `[Event]` | +| Registered Services | Any service registered in DI container | No | +| `ILambdaHostContext` | Framework context with Lambda metadata | No | +| `CancellationToken` | For cancellation and timeout handling | No | + +**Example:** + +```csharp +lambda.MapHandler(async ( + [Event] Order order, // Lambda event (required) + IOrderService orderService, // Registered service + ICache cache, // Another service + ILambdaHostContext context, // Framework context + CancellationToken cancellationToken // Cancellation token +) => +{ + // Access Lambda metadata + context.Items["OrderId"] = order.Id; + context.Items["StartTime"] = DateTime.UtcNow; + + // Check cache + if (cache.TryGet(order.Id, out var cached)) + return cached; + + // Process with cancellation support + var result = await orderService.ProcessAsync(order, cancellationToken); + + // Update cache + cache.Set(order.Id, result); + + return result; +}); +``` + +### Service Registration Patterns + +```csharp +var builder = LambdaApplication.CreateBuilder(); + +// === Singleton Services (shared) === +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Singleton with factory +builder.Services.AddSingleton(sp => + new ConnectionPool(sp.GetRequiredService()) +); + +// === Scoped Services (per invocation) === +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Scoped with interface injection +builder.Services.AddScoped(sp => + new RequestContext(sp.GetRequiredService()) +); + +// === Configuration === +builder.Services.Configure( + builder.Configuration.GetSection("AppSettings") +); +``` + +## Middleware Pipeline + +Middleware provides a way to compose request processing logic in a pipeline. Each middleware can execute code before and after the next middleware (or handler) in the pipeline. + +### How Middleware Works + +Middleware forms a chain where each component can: + +1. **Pre-process**: Execute logic before the handler +2. **Invoke next**: Call the next middleware/handler +3. **Post-process**: Execute logic after the handler + +### Basic Middleware Pattern + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + // === PRE-PROCESSING === + // Runs BEFORE the handler + Console.WriteLine("Before handler execution"); + var stopwatch = Stopwatch.StartNew(); + + try + { + // === CALL NEXT MIDDLEWARE/HANDLER === + await next(context); + + // === POST-PROCESSING (Success) === + // Runs AFTER the handler (success path) + stopwatch.Stop(); + Console.WriteLine($"Handler succeeded in {stopwatch.ElapsedMilliseconds}ms"); + } + catch (Exception ex) + { + // === POST-PROCESSING (Error) === + // Runs AFTER the handler (error path) + stopwatch.Stop(); + Console.WriteLine($"Handler failed after {stopwatch.ElapsedMilliseconds}ms: {ex.Message}"); + throw; // Re-throw to propagate error + } +}); +``` + +### Multiple Middleware Execution Order + +Middleware executes in a **nested** fashion—like Russian dolls: + +```csharp +// Register middleware in order +lambda.UseMiddleware(/* MW1: Logging */); +lambda.UseMiddleware(/* MW2: Metrics */); +lambda.UseMiddleware(/* MW3: Validation */); +lambda.MapHandler(/* Handler */); +``` + +**Execution flow:** + +```mermaid +sequenceDiagram + participant Request + participant MW1 + participant MW2 + participant MW3 + participant Handler + + Request->>MW1: Enter + Note over MW1: Pre-process + MW1->>MW2: next() + Note over MW2: Pre-process + MW2->>MW3: next() + Note over MW3: Pre-process + MW3->>Handler: next() + Note over Handler: Execute + Handler-->>MW3: Return + Note over MW3: Post-process + MW3-->>MW2: Return + Note over MW2: Post-process + MW2-->>MW1: Return + Note over MW1: Post-process + MW1-->>Request: Response +``` + +### Middleware Use Cases + +#### 1. Logging Middleware + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request started"); + await next(context); + Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request completed"); +}); +``` + +#### 2. Timing/Performance Middleware + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + var stopwatch = Stopwatch.StartNew(); + await next(context); + stopwatch.Stop(); + + Console.WriteLine($"Execution time: {stopwatch.ElapsedMilliseconds}ms"); + + if (stopwatch.ElapsedMilliseconds > 1000) + Console.WriteLine("WARNING: Slow execution detected"); +}); +``` + +#### 3. Error Handling Middleware + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch (ValidationException ex) + { + Console.WriteLine($"Validation error: {ex.Message}"); + // Handle validation errors + throw; + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected error: {ex}"); + // Log to monitoring service + throw; + } +}); +``` + +#### 4. Correlation ID Middleware + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + var correlationId = Guid.NewGuid().ToString(); + context.Items["CorrelationId"] = correlationId; + Console.WriteLine($"Correlation ID: {correlationId}"); + + await next(context); +}); +``` + +### Middleware vs Handlers Comparison + +| Feature | Middleware | Handler | +|---------|-----------|---------| +| **Execution** | Every invocation | Single invocation only | +| **Parameters** | `ILambdaHostContext`, `Func next` | `[Event]` + Services | +| **Return Type** | `void` or `Task` | Event response type | +| **Purpose** | Cross-cutting concerns | Business logic | +| **Count** | Multiple (forms pipeline) | One per Lambda | +| **When to Use** | Logging, metrics, validation | Domain logic | + +## Handler Registration + +Handlers are where your business logic lives. The framework uses source generation to create optimized, type-safe handlers. + +### Basic Handler + +The simplest handler takes an input and returns an output: + +```csharp +lambda.MapHandler(([Event] string input) => input.ToUpper()); +``` + +### Handler with Services + +Inject services directly into your handler: + +```csharp +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.Process(order) +); +``` + +### Handler with Multiple Dependencies + +Inject as many services as you need: + +```csharp +lambda.MapHandler(([Event] Order order, + IOrderService orderService, + IInventoryService inventoryService, + IPaymentService paymentService) => +{ + // All services are automatically injected + var inventory = inventoryService.CheckAvailability(order); + var payment = paymentService.ProcessPayment(order); + return orderService.FulfillOrder(order, inventory, payment); +}); +``` + +### Async Handlers + +Use async/await for I/O operations: + +```csharp +lambda.MapHandler(async ([Event] Order order, + IOrderRepository repository, + CancellationToken cancellationToken) => +{ + var result = await repository.SaveAsync(order, cancellationToken); + return new OrderResponse(result.Id, true); +}); +``` + +### Handler with Context + +Access Lambda context information: + +```csharp +lambda.MapHandler(([Event] Request request, ILambdaHostContext context) => +{ + // Store metadata in context items + context.Items["RequestId"] = request.Id; + context.Items["ProcessedAt"] = DateTime.UtcNow; + + // Process request... + return new Response("Success"); +}); +``` + +### The [Event] Attribute + +The `[Event]` attribute is **required** on exactly one parameter to mark the Lambda event. + +**Rules:** +- Must be used on exactly one parameter +- Marks the parameter that receives the deserialized Lambda event +- Triggers source generation for type-safe deserialization +- Enables compile-time validation + +**Example:** + +```csharp +// ✅ CORRECT: One [Event] parameter +lambda.MapHandler(([Event] Order order, IService service) => { ... }); + +// ❌ WRONG: No [Event] parameter +lambda.MapHandler((Order order, IService service) => { ... }); + +// ❌ WRONG: Multiple [Event] parameters +lambda.MapHandler(([Event] Order order, [Event] string id) => { ... }); +``` + +## Source Generation + +aws-lambda-host uses C# source generators and interceptors to optimize Lambda performance at compile time. + +### What Happens at Compile Time + +When you write: + +```csharp +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.Process(order) +); +``` + +The source generator: + +1. **Analyzes** the handler signature during compilation +2. **Validates** that `[Event]` is used correctly +3. **Generates** deserialization code for the Order type +4. **Generates** dependency injection resolution code +5. **Injects** optimized code via interceptors + +### Benefits + +#### Zero Runtime Reflection + +Traditional frameworks use reflection to inspect types and resolve dependencies at runtime. Source generation moves this to compile time. + +**Result**: Faster execution, especially during cold starts. + +#### AOT Compilation Ready + +Source generation enables Native AOT compilation because all type information is resolved at compile time. + +**Result**: Smallest possible package size and fastest cold starts. + +#### Compile-Time Errors + +Type mismatches and configuration errors are caught during compilation, not at runtime. + +**Result**: Fewer production bugs and faster feedback during development. + +#### Better Performance + +Generated code is optimized specifically for your handler signature. + +**Result**: Minimal overhead, maximum throughput. + +### Source Generation Example + +Your code: + +```csharp +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.Process(order) +); +``` + +Generated code (simplified): + +```csharp +// Generated by source generator +private static async Task HandleInvocation( + Stream inputStream, + IServiceProvider services) +{ + // Deserialize event (no reflection) + var order = JsonSerializer.Deserialize(inputStream); + + // Resolve services (no reflection) + var service = services.GetRequiredService(); + + // Execute handler + var result = service.Process(order); + + // Serialize response + return JsonSerializer.Serialize(result); +} +``` + +!!! info "Transparent Optimization" + You don't need to think about source generation—it just works. Write clean, simple handler code and let the framework optimize it. + +## Putting It All Together + +Here's a complete example demonstrating all core concepts: + +```csharp +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +// === MODELS === +public record Order(string Id, decimal Amount); +public record OrderResult(string OrderId, bool Success); + +// === SERVICES === +public interface ICache +{ + Task WarmupAsync(CancellationToken ct); + bool TryGet(string key, out OrderResult value); + void Set(string key, OrderResult value); +} + +public interface IOrderRepository +{ + Task ProcessAsync(Order order, CancellationToken ct); +} + +// === SETUP === +var builder = LambdaApplication.CreateBuilder(); + +// Register services with appropriate lifetimes +builder.Services.AddSingleton(); // Shared across invocations +builder.Services.AddScoped(); // Per invocation + +var lambda = builder.Build(); + +// === LIFECYCLE: OnInit (runs once) === +lambda.OnInit(async (services, cancellationToken) => +{ + Console.WriteLine("=== Initializing Lambda ==="); + var cache = services.GetRequiredService(); + await cache.WarmupAsync(cancellationToken); + Console.WriteLine("=== Initialization Complete ==="); + return true; +}); + +// === MIDDLEWARE 1: Logging === +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine($"[{DateTime.UtcNow}] Request started"); + await next(context); + Console.WriteLine($"[{DateTime.UtcNow}] Request completed"); +}); + +// === MIDDLEWARE 2: Timing === +lambda.UseMiddleware(async (context, next) => +{ + var stopwatch = Stopwatch.StartNew(); + await next(context); + stopwatch.Stop(); + Console.WriteLine($"Duration: {stopwatch.ElapsedMilliseconds}ms"); +}); + +// === MIDDLEWARE 3: Correlation ID === +lambda.UseMiddleware(async (context, next) => +{ + var correlationId = Guid.NewGuid().ToString(); + context.Items["CorrelationId"] = correlationId; + Console.WriteLine($"Correlation ID: {correlationId}"); + await next(context); +}); + +// === HANDLER: Business Logic with DI === +lambda.MapHandler(async ( + [Event] Order order, // Lambda event + IOrderRepository repository, // Scoped service + ICache cache, // Singleton service + ILambdaHostContext context, // Framework context + CancellationToken cancellationToken // Cancellation token +) => +{ + // Check cache first + if (cache.TryGet(order.Id, out var cached)) + { + Console.WriteLine($"Cache hit for order {order.Id}"); + return cached; + } + + // Process order + Console.WriteLine($"Processing order {order.Id}"); + var result = await repository.ProcessAsync(order, cancellationToken); + + // Update cache + cache.Set(order.Id, result); + + return result; +}); + +// === LIFECYCLE: OnShutdown (runs once) === +lambda.OnShutdown(async (services, cancellationToken) => +{ + Console.WriteLine("=== Shutting Down Lambda ==="); + var cache = services.GetRequiredService(); + // Flush cache or perform cleanup + Console.WriteLine("=== Shutdown Complete ==="); +}); + +// === RUN === +await lambda.RunAsync(); +``` + +## Key Takeaways + +Now you understand the core concepts of aws-lambda-host: + +1. **Lambda Lifecycle** + - OnInit runs once on cold start (setup) + - Invocation runs for each event (business logic) + - OnShutdown runs once before termination (cleanup) + +2. **Dependency Injection** + - Singleton: Created once, shared across invocations (must be thread-safe) + - Scoped: Created per invocation, isolated and disposed after + +3. **Middleware Pipeline** + - Executes before and after handlers + - Perfect for cross-cutting concerns + - Composes into a nested pipeline + +4. **Handler Registration** + - Use `[Event]` to mark the Lambda event parameter + - Inject services directly into handlers + - Framework resolves dependencies automatically + +5. **Source Generation** + - Compile-time code generation + - Zero runtime reflection + - AOT compilation ready + - Compile-time validation + +## Next Steps + +Now that you understand the core concepts, learn how to organize your Lambda projects effectively: + +**→ [Project Structure](project-structure.md)** – Learn best practices for organizing your code, managing configuration, and structuring tests. + +### Explore Further + +- **[Middleware Guide](/guides/middleware.md)** – Advanced middleware patterns +- **[Dependency Injection Guide](/guides/dependency-injection.md)** – DI best practices +- **[Lifecycle Management](/guides/lifecycle-management.md)** – Deep dive into lifecycle hooks +- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns +- **[Source Generators](/advanced/source-generators.md)** – How source generation works internally diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md new file mode 100644 index 00000000..2bb7008b --- /dev/null +++ b/docs/getting-started/first-lambda.md @@ -0,0 +1,602 @@ +# Your First Lambda Function + +This tutorial walks you through building, testing, and deploying a complete Lambda function using aws-lambda-host. You'll create a greeting service that demonstrates dependency injection, middleware, and type-safe handlers. + +## What We'll Build + +A greeting service Lambda function with: + +- ✅ Strongly-typed request/response models +- ✅ Dependency injection with a custom service +- ✅ Middleware for logging +- ✅ Async/await patterns +- ✅ Local testing +- ✅ AWS deployment + +**Time to Complete**: ~20 minutes + +## Prerequisites + +Before starting, ensure you've completed the [Installation](installation.md) guide and have: + +- aws-lambda-host NuGet package installed +- Project file properly configured +- .NET 8 SDK installed + +## Step 1: Define Your Models + +Create strongly-typed models for your Lambda's input and output using C# records. + +Add this code to your `Program.cs`: + +```csharp title="Program.cs" linenums="1" +using System; +using System.Text.Json.Serialization; + +// Request model - what your Lambda receives +public record GreetingRequest( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("language")] string? Language +); + +// Response model - what your Lambda returns +public record GreetingResponse( + [property: JsonPropertyName("message")] string Message, + [property: JsonPropertyName("timestamp")] DateTime Timestamp +); +``` + +!!! tip "Why Records?" + Records provide immutable data models with built-in equality and deconstruction. They're perfect for Lambda events and responses. + +!!! note "JsonPropertyName Attribute" + The `JsonPropertyName` attribute ensures your JSON property names follow your preferred casing convention (e.g., camelCase for JavaScript clients). + +## Step 2: Create a Service Interface + +Define an interface for your business logic. This enables dependency injection and testability. + +```csharp title="Program.cs (continued)" +// Service interface +public interface IGreetingService +{ + string GetGreeting(string name, string? language); +} +``` + +## Step 3: Implement the Service + +Implement your service with the actual greeting logic. + +```csharp title="Program.cs (continued)" +// Service implementation +public class GreetingService : IGreetingService +{ + private static readonly Dictionary Greetings = new() + { + { "en", "Hello" }, + { "es", "Hola" }, + { "fr", "Bonjour" }, + { "de", "Guten Tag" }, + { "it", "Ciao" }, + { "ja", "こんにちは" } + }; + + public string GetGreeting(string name, string? language) + { + var greeting = Greetings.GetValueOrDefault(language ?? "en", "Hello"); + return $"{greeting}, {name}!"; + } +} +``` + +## Step 4: Configure the Lambda Application + +Set up the Lambda application builder and register your services with dependency injection. + +```csharp title="Program.cs (continued)" +using AwsLambda.Host.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +// Create the Lambda application builder +var builder = LambdaApplication.CreateBuilder(); + +// Register the greeting service +builder.Services.AddSingleton(); + +// Build the Lambda application +var lambda = builder.Build(); +``` + +!!! info "Service Lifetime: Singleton" + We use `AddSingleton` because our `GreetingService` is stateless and can be shared across all invocations. This is more efficient than creating a new instance for each request. + +## Step 5: Add Middleware (Optional) + +Add middleware to log when requests are received and completed. Middleware runs before and after your handler. + +```csharp title="Program.cs (continued)" +// Add logging middleware +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request received"); + + // Execute the next middleware/handler in the pipeline + await next(context); + + Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request completed"); +}); +``` + +!!! tip "Middleware Use Cases" + Middleware is perfect for cross-cutting concerns like logging, metrics, validation, error handling, and authentication. + +## Step 6: Register the Handler + +Map your handler function with the `[Event]` attribute to mark the Lambda event parameter. + +```csharp title="Program.cs (continued)" +// Register the handler with dependency injection +lambda.MapHandler(([Event] GreetingRequest request, IGreetingService service) => +{ + // Call the service to get the greeting + var message = service.GetGreeting(request.Name, request.Language); + + // Return the response + return new GreetingResponse(message, DateTime.UtcNow); +}); +``` + +!!! warning "The [Event] Attribute" + The `[Event]` attribute is required on exactly one parameter. It tells the framework which parameter receives the deserialized Lambda event. + +## Step 7: Run the Lambda + +Start the Lambda runtime to listen for invocations. + +```csharp title="Program.cs (continued)" +// Run the Lambda function +await lambda.RunAsync(); +``` + +## Complete Program.cs + +Here's the complete code all together: + +```csharp title="Program.cs" linenums="1" +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using AwsLambda.Host.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +// Request and Response models +public record GreetingRequest( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("language")] string? Language +); + +public record GreetingResponse( + [property: JsonPropertyName("message")] string Message, + [property: JsonPropertyName("timestamp")] DateTime Timestamp +); + +// Service interface +public interface IGreetingService +{ + string GetGreeting(string name, string? language); +} + +// Service implementation +public class GreetingService : IGreetingService +{ + private static readonly Dictionary Greetings = new() + { + { "en", "Hello" }, + { "es", "Hola" }, + { "fr", "Bonjour" }, + { "de", "Guten Tag" }, + { "it", "Ciao" }, + { "ja", "こんにちは" } + }; + + public string GetGreeting(string name, string? language) + { + var greeting = Greetings.GetValueOrDefault(language ?? "en", "Hello"); + return $"{greeting}, {name}!"; + } +} + +// Lambda application setup +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +// Middleware +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request received"); + await next(context); + Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request completed"); +}); + +// Handler +lambda.MapHandler(([Event] GreetingRequest request, IGreetingService service) => +{ + var message = service.GetGreeting(request.Name, request.Language); + return new GreetingResponse(message, DateTime.UtcNow); +}); + +// Run +await lambda.RunAsync(); +``` + +## Testing Locally + +Before deploying to AWS, test your Lambda function locally using the AWS Lambda Test Tool. + +### Install the Lambda Test Tool + +Install the tool globally using .NET CLI: + +```bash +dotnet tool install -g Amazon.Lambda.TestTool-8.0 +``` + +### Run the Test Tool + +Start the test tool from your project directory: + +```bash +dotnet lambda-test-tool-8.0 +``` + +This opens a web browser with a testing interface. + +### Create a Test Payload + +In the test tool, enter this JSON payload: + +```json title="Test Payload" +{ + "name": "World", + "language": "en" +} +``` + +### Execute and Verify + +Click **Execute Function** and verify the response: + +```json title="Expected Response" +{ + "message": "Hello, World!", + "timestamp": "2025-11-29T12:34:56.789Z" +} +``` + +### Test Different Languages + +Try different payloads: + +=== "Spanish" + + ```json + { + "name": "Mundo", + "language": "es" + } + ``` + + Expected: `"message": "Hola, Mundo!"` + +=== "French" + + ```json + { + "name": "Monde", + "language": "fr" + } + ``` + + Expected: `"message": "Bonjour, Monde!"` + +=== "Japanese" + + ```json + { + "name": "世界", + "language": "ja" + } + ``` + + Expected: `"message": "こんにちは, 世界!"` + +### Check the Logs + +In the test tool output, you should see the middleware logs: + +``` +[2025-11-29 12:34:56] Request received +[2025-11-29 12:34:56] Request completed +``` + +## Deploying to AWS + +Now that your Lambda works locally, deploy it to AWS. Choose your preferred deployment method: + +=== "AWS SAM" + + ### Create SAM Template + + Create a `template.yaml` file in your project root: + + ```yaml title="template.yaml" + AWSTemplateFormatVersion: '2010-09-09' + Transform: AWS::Serverless-2016-10-31 + Description: Greeting Lambda Function + + Resources: + GreetingFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: greeting-function + Handler: bootstrap + Runtime: provided.al2023 + CodeUri: ./ + MemorySize: 512 + Timeout: 30 + Architectures: + - x86_64 + + Outputs: + GreetingFunctionArn: + Description: ARN of the Greeting Lambda Function + Value: !GetAtt GreetingFunction.Arn + ``` + + ### Build and Deploy + + ```bash + # Build the Lambda + sam build + + # Deploy (first time - guided) + sam deploy --guided + + # Follow prompts: + # - Stack Name: greeting-lambda-stack + # - AWS Region: us-east-1 (or your preferred region) + # - Confirm changes: Y + # - Allow SAM CLI IAM role creation: Y + ``` + + ### Invoke the Function + + ```bash + sam local invoke GreetingFunction --event event.json + ``` + + Create `event.json`: + + ```json + { + "name": "World", + "language": "en" + } + ``` + +=== "AWS CDK (C#)" + + ### Install CDK + + ```bash + npm install -g aws-cdk + ``` + + ### Create CDK Stack + + ```csharp title="GreetingStack.cs" + using Amazon.CDK; + using Amazon.CDK.AWS.Lambda; + using Constructs; + + public class GreetingStack : Stack + { + public GreetingStack(Construct scope, string id, IStackProps props = null) + : base(scope, id, props) + { + new Function(this, "GreetingFunction", new FunctionProps + { + Runtime = Runtime.PROVIDED_AL2023, + Handler = "bootstrap", + Code = Code.FromAsset("./publish"), + MemorySize = 512, + Timeout = Duration.Seconds(30), + FunctionName = "greeting-function" + }); + } + } + ``` + + ### Deploy + + ```bash + # Publish the Lambda + dotnet publish -c Release + + # Deploy the stack + cdk deploy + ``` + +=== "Manual Deployment" + + ### Publish the Lambda + + ```bash + dotnet publish -c Release -o ./publish + ``` + + ### Create Deployment Package + + ```bash + cd publish + zip -r ../function.zip . + cd .. + ``` + + ### Create IAM Role + + Create an execution role with basic Lambda permissions in the AWS Console or using AWS CLI. + + ### Deploy with AWS CLI + + ```bash + aws lambda create-function \ + --function-name greeting-function \ + --runtime provided.al2023 \ + --handler bootstrap \ + --zip-file fileb://function.zip \ + --role arn:aws:iam::YOUR_ACCOUNT_ID:role/lambda-execution-role \ + --timeout 30 \ + --memory-size 512 + ``` + + Replace `YOUR_ACCOUNT_ID` with your AWS account ID. + +## Invoking the Deployed Function + +Test your deployed Lambda function using the AWS CLI: + +```bash +# Create test event +echo '{"name":"World","language":"es"}' > event.json + +# Invoke the function +aws lambda invoke \ + --function-name greeting-function \ + --payload file://event.json \ + response.json + +# View the response +cat response.json +``` + +Expected output in `response.json`: + +```json +{ + "message": "Hola, World!", + "timestamp": "2025-11-29T12:34:56.789Z" +} +``` + +## Request Flow Diagram + +Here's how a request flows through your Lambda function: + +```mermaid +sequenceDiagram + participant Client + participant Lambda Runtime + participant Middleware + participant Handler + participant Service + + Client->>Lambda Runtime: GreetingRequest JSON + Lambda Runtime->>Lambda Runtime: Deserialize to GreetingRequest + Lambda Runtime->>Middleware: Process Request + Middleware->>Middleware: Log "Request received" + Middleware->>Handler: Execute Handler + Handler->>Service: GetGreeting(name, language) + Service-->>Handler: "Hello, World!" + Handler-->>Middleware: GreetingResponse + Middleware->>Middleware: Log "Request completed" + Middleware-->>Lambda Runtime: GreetingResponse + Lambda Runtime->>Lambda Runtime: Serialize to JSON + Lambda Runtime-->>Client: Response JSON +``` + +## Common Issues + +### Handler Not Found Error + +**Error**: `Handler 'bootstrap' not found` + +**Solution**: Ensure your project has `Exe` in the `.csproj` file. + +### JSON Serialization Error + +**Error**: Properties are not being serialized correctly + +**Solution**: Verify you've added `[property: JsonPropertyName("...")]` attributes to your record properties. + +### Build Errors with [Event] Attribute + +**Error**: `The [Event] attribute is not recognized` + +**Solution**: Ensure you have the `InterceptorsNamespaces` configuration in your `.csproj`: + +```xml +$(InterceptorsNamespaces);AwsLambda.Host +``` + +### Timeout Errors + +**Error**: Lambda times out during execution + +**Solution**: +- Increase timeout in your deployment template (default is 30 seconds) +- Check for blocking operations in your handler +- Ensure async/await is used correctly + +### Service Not Injected + +**Error**: Service parameter is null in handler + +**Solution**: Verify the service is registered in `builder.Services` before calling `builder.Build()`. + +## What You've Learned + +Congratulations! You've built and deployed a complete Lambda function. You now understand: + +- ✅ How to structure Lambda functions with aws-lambda-host +- ✅ Defining strongly-typed request/response models +- ✅ Creating and registering services with dependency injection +- ✅ Adding middleware for cross-cutting concerns +- ✅ Using the `[Event]` attribute for handler parameters +- ✅ Testing locally with the Lambda Test Tool +- ✅ Deploying to AWS with SAM, CDK, or manual methods + +## Next Steps + +Now that you have a working Lambda function, dive deeper into the framework: + +### Learn Core Concepts + +**→ [Core Concepts](core-concepts.md)** – Understand the Lambda lifecycle, DI patterns, middleware pipeline, and source generation. + +### Explore Advanced Features + +- **[Middleware Patterns](/guides/middleware.md)** – Build reusable middleware components +- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns +- **[Testing Strategies](/guides/testing.md)** – Unit and integration testing +- **[Deployment Best Practices](/guides/deployment.md)** – CI/CD and production deployments + +### Add Type-Safe Event Handling + +- **[SQS Envelope](/features/envelopes/sqs.md)** – Process SQS messages +- **[API Gateway Envelope](/features/envelopes/api-gateway.md)** – Build HTTP APIs +- **[SNS Envelope](/features/envelopes/sns.md)** – Handle SNS notifications + +### Enhance Observability + +- **[OpenTelemetry Integration](/features/opentelemetry.md)** – Add distributed tracing and metrics + +### Optimize Performance + +- **[AOT Compilation](/advanced/aot-compilation.md)** – Achieve fastest cold starts +- **[Performance Optimization](/advanced/performance-optimization.md)** – Profiling and tuning diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 00000000..3ca0eab5 --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,216 @@ +# Getting Started + +Welcome to **aws-lambda-host**, a modern .NET framework for building AWS Lambda functions using familiar .NET patterns and best practices. + +## What is aws-lambda-host? + +aws-lambda-host is a .NET hosting framework that brings the familiar patterns from ASP.NET Core to AWS Lambda development. Instead of writing boilerplate code to handle Lambda events, context, and serialization, you get a clean, declarative API for defining Lambda handlers with dependency injection, middleware support, and modern async/await patterns. + +Built on the generic host from Microsoft.Extensions, it provides a .NET hosting experience similar to ASP.NET Core but tailored specifically for Lambda execution. + +## Why Use This Framework? + +### Traditional Lambda Approach + +```csharp +public class Function +{ + public string FunctionHandler(string input, ILambdaContext context) + { + // Manual initialization + // Manual dependency management + // Manual error handling + return input.ToUpper(); + } +} +``` + +### aws-lambda-host Approach + +```csharp +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddScoped(); + +var lambda = builder.Build(); +lambda.MapHandler(([Event] string input, IMyService service) => + service.ProcessAsync(input)); + +await lambda.RunAsync(); +``` + +### Key Benefits + +- **Familiar Patterns** – Use the same .NET hosting patterns you know from ASP.NET Core +- **Dependency Injection** – Built-in DI container with proper scoped lifetime management +- **Middleware Pipeline** – Compose cross-cutting concerns like logging, validation, and error handling +- **Source Generation** – Compile-time code generation eliminates reflection overhead +- **AOT Ready** – Full support for Ahead-of-Time compilation for faster cold starts +- **Type Safety** – Strongly-typed event handlers with compile-time validation + +## Prerequisites + +Before you begin, ensure you have: + +- **.NET 8 SDK or later** – [Download here](https://dotnet.microsoft.com/download) +- **C# 11 or later** – Required for source generators and language features +- **Basic AWS Lambda knowledge** – Understanding of Lambda concepts (functions, events, execution model) +- **AWS Account** – For deploying and testing (optional for local development) + +!!! tip "IDE Recommendations" + - Visual Studio 2022 (17.8+) + - JetBrains Rider 2023.3+ + - Visual Studio Code with C# Dev Kit + +## What You'll Learn + +This Getting Started guide will walk you through everything you need to build production-ready Lambda functions: + +### 1. [Installation](installation.md) (~10 minutes) +- Install NuGet packages +- Configure your project file +- Verify your setup + +### 2. [Your First Lambda](first-lambda.md) (~20 minutes) +- Build a complete Lambda function step-by-step +- Add dependency injection +- Test locally +- Deploy to AWS + +### 3. [Core Concepts](core-concepts.md) (~30 minutes) +- Understand the Lambda lifecycle (OnInit, Invocation, OnShutdown) +- Master dependency injection patterns +- Work with middleware pipelines +- Learn about source generation + +### 4. [Project Structure](project-structure.md) (~15 minutes) +- Organize your Lambda projects +- Follow best practices +- Avoid common anti-patterns +- Structure for maintainability + +**Total Time**: ~85 minutes from zero to productive Lambda developer + +## Framework Philosophy + +aws-lambda-host is built on these core principles: + +### .NET Hosting Patterns + +Use the builder pattern, dependency injection, and middleware—the same patterns that make ASP.NET Core productive and maintainable. + +### Async-First Design + +Native support for async/await throughout the framework, with proper Lambda timeout and cancellation handling built in. + +### Source Generation Benefits + +Source generators analyze your handler code at compile time, generating optimized deserialization and DI resolution code. This eliminates reflection overhead and enables AOT compilation. + +### Minimal Runtime Overhead + +No unnecessary abstractions or reflection at runtime. The framework is designed for Lambda's constraints: fast cold starts, efficient memory usage, and predictable performance. + +### Progressive Enhancement + +Start simple with basic handlers, then add middleware, observability, and advanced features as your needs grow. + +## Quick Example + +Here's a complete Lambda function that demonstrates the framework's key features: + +```csharp +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +// Define your models +public record OrderRequest(string OrderId, decimal Amount); +public record OrderResponse(bool Success, string Message); + +// Define your service +public interface IOrderService +{ + Task ProcessAsync(OrderRequest order); +} + +public class OrderService : IOrderService +{ + public async Task ProcessAsync(OrderRequest order) + { + // Business logic here + await Task.Delay(100); // Simulate processing + return new OrderResponse(true, $"Order {order.OrderId} processed"); + } +} + +// Configure and run +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddScoped(); + +var lambda = builder.Build(); + +// Add middleware for logging +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine($"Processing request at {DateTime.UtcNow}"); + await next(context); + Console.WriteLine($"Request completed at {DateTime.UtcNow}"); +}); + +// Register handler with dependency injection +lambda.MapHandler(async ([Event] OrderRequest request, IOrderService service) => + await service.ProcessAsync(request)); + +await lambda.RunAsync(); +``` + +This example shows: + +- ✅ Strongly-typed request/response models using records +- ✅ Service interface and implementation +- ✅ Dependency injection registration +- ✅ Middleware for cross-cutting concerns +- ✅ Handler with automatic DI resolution +- ✅ Async/await throughout + +## Next Steps + +Ready to start building? Choose your path: + +### For Beginners + +Follow the guide in order: + +1. **[Installation →](installation.md)** – Set up your environment +2. **[Your First Lambda →](first-lambda.md)** – Build a complete example +3. **[Core Concepts →](core-concepts.md)** – Understand the framework +4. **[Project Structure →](project-structure.md)** – Organize your code + +### For Experienced Developers + +Jump to what you need: + +- **[Installation](installation.md)** – Quick setup reference +- **[Core Concepts](core-concepts.md)** – Deep dive into architecture +- **[Guides](/guides/)** – Comprehensive feature documentation +- **[Examples](/examples/)** – Complete working examples +- **[API Reference](/api-reference/)** – Detailed API docs + +### Explore Features + +- **[Envelopes](/features/envelopes/)** – Type-safe event source integration (SQS, SNS, API Gateway, etc.) +- **[OpenTelemetry](/features/opentelemetry.md)** – Distributed tracing and observability +- **[AOT Compilation](/advanced/aot-compilation.md)** – Optimize for fastest cold starts +- **[Source Generators](/advanced/source-generators.md)** – Understand compile-time optimizations + +## Getting Help + +If you run into issues or have questions: + +- **[FAQ](/resources/faq.md)** – Common questions and answers +- **[Troubleshooting](/resources/troubleshooting.md)** – Solutions to common problems +- **[GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues)** – Report bugs or request features +- **[GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions)** – Ask questions and share ideas + +--- + +Let's get started! Head to **[Installation →](installation.md)** to set up your environment. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 00000000..e72092ca --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,269 @@ +# Installation + +This guide walks you through installing aws-lambda-host and configuring your project to build Lambda functions. + +## System Requirements + +Before you begin, ensure your development environment meets these requirements: + +| Requirement | Minimum Version | Recommended | +|------------|----------------|-------------| +| .NET SDK | 8.0 | Latest LTS | +| C# Language Version | 11 | latest | +| IDE | Visual Studio 2022 (17.8+), Rider 2023.3+, or VS Code | Latest | +| AWS CLI | 2.0+ (optional) | Latest | + +!!! note "C# 11 Requirement" + C# 11 or later is required for source generators and interceptors that power the framework's compile-time optimizations. + +## Installing the NuGet Package + +Choose your preferred installation method: + +=== ".NET CLI" + + ```bash + # Create a new console project + dotnet new console -n MyFirstLambda + cd MyFirstLambda + + # Add the AwsLambda.Host package + dotnet add package AwsLambda.Host + ``` + +=== "Visual Studio" + + 1. Right-click on your project in Solution Explorer + 2. Select **Manage NuGet Packages** + 3. Search for `AwsLambda.Host` + 4. Click **Install** + +=== "Package Reference" + + Add this to your `.csproj` file: + + ```xml + + + + ``` + +## Project File Configuration + +Configure your project file (`.csproj`) with the required settings for Lambda development. + +### Required Settings + +Open your `.csproj` file and ensure these properties are set: + +```xml title="MyFirstLambda.csproj" + + + + Exe + + + net8.0 + + + latest + + + Lambda + + + enable + + + disable + + +``` + +### Source Generator Configuration + +Add this property to enable source generators and interceptors: + +```xml title="MyFirstLambda.csproj" + + + $(InterceptorsNamespaces);AwsLambda.Host + +``` + +!!! warning "InterceptorsNamespaces Required" + Without this setting, the framework's source generators won't work correctly and you'll get compilation errors. + +### Optional Settings (Recommended) + +These optional settings improve the Lambda development experience: + +```xml title="MyFirstLambda.csproj" + + + true + + + true + + + true + +``` + +### Complete Example + +Here's a complete, minimal `.csproj` file for a Lambda function: + +```xml title="MyFirstLambda.csproj" linenums="1" + + + Exe + net8.0 + latest + disable + enable + Lambda + true + true + $(InterceptorsNamespaces);AwsLambda.Host + + + + + + +``` + +## Verifying Installation + +Let's verify everything is set up correctly by creating a simple Lambda function. + +### Create Program.cs + +Create a `Program.cs` file with this minimal example: + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host; + +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +lambda.MapHandler(([Event] string input) => input.ToUpper()); + +await lambda.RunAsync(); +``` + +### Build the Project + +Run the build command: + +=== ".NET CLI" + + ```bash + dotnet build + ``` + +=== "Visual Studio" + + Press `Ctrl+Shift+B` or select **Build** → **Build Solution** + +### Expected Output + +You should see output similar to: + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +!!! success "Installation Successful" + If the build succeeds, your installation is complete and you're ready to build Lambda functions! + +## Package Overview + +The aws-lambda-host framework includes multiple packages for different use cases: + +### Core Packages + +| Package | Purpose | When to Use | +|---------|---------|-------------| +| **AwsLambda.Host** | Core framework | Required for all Lambda functions | +| **AwsLambda.Host.Abstractions** | Interfaces and contracts | When creating custom extensions or middleware | +| **AwsLambda.Host.OpenTelemetry** | Observability integration | When you need distributed tracing and metrics | + +### Envelope Packages + +Envelope packages provide type-safe, strongly-typed event handling for specific AWS event sources: + +| Package | Event Source | When to Use | +|---------|--------------|-------------| +| **AwsLambda.Host.Envelopes.Sqs** | Amazon SQS | Processing SQS queue messages | +| **AwsLambda.Host.Envelopes.Sns** | Amazon SNS | Handling SNS notifications | +| **AwsLambda.Host.Envelopes.ApiGateway** | API Gateway | Building REST/HTTP APIs | +| **AwsLambda.Host.Envelopes.Kinesis** | Kinesis Data Streams | Processing stream records | +| **AwsLambda.Host.Envelopes.KinesisFirehose** | Kinesis Firehose | Transforming Firehose data | +| **AwsLambda.Host.Envelopes.Kafka** | Apache Kafka / MSK | Processing Kafka messages | +| **AwsLambda.Host.Envelopes.CloudWatchLogs** | CloudWatch Logs | Processing log subscriptions | +| **AwsLambda.Host.Envelopes.Alb** | Application Load Balancer | ALB target Lambda functions | + +!!! info "Envelope Packages" + You only need envelope packages if you're working with those specific event sources. For simple use cases, just `AwsLambda.Host` is sufficient. Learn more in the [Envelopes documentation](/features/envelopes/). + +## Troubleshooting + +### Common Issues + +#### C# Language Version Error + +**Error**: `Feature 'interceptors' is not available in C# 10` + +**Solution**: Set `latest` in your `.csproj` file. + +#### Source Generator Warning + +**Warning**: `Interceptor must be in a namespace annotated with 'InterceptsLocationAttribute'` + +**Solution**: Add the `InterceptorsNamespaces` property to your `.csproj`: + +```xml +$(InterceptorsNamespaces);AwsLambda.Host +``` + +#### Build Errors After Installation + +**Error**: Various build errors after adding the package + +**Solution**: +1. Verify your .NET SDK version: `dotnet --version` +2. Ensure it's .NET 8.0 or later +3. Clean and rebuild: `dotnet clean && dotnet build` + +#### Missing OutputType + +**Error**: Lambda function doesn't execute when deployed + +**Solution**: Ensure `Exe` is set in your `.csproj`. Lambda functions must be executable console applications. + +### Getting Help + +If you encounter issues not covered here: + +- Check the [Troubleshooting Guide](/resources/troubleshooting.md) +- Review [Common Questions](/resources/faq.md) +- Search or ask in [GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions) +- Report bugs in [GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues) + +## Next Steps + +Now that you have aws-lambda-host installed and verified, you're ready to build your first Lambda function! + +**→ Continue to [Your First Lambda](first-lambda.md)** to build a complete Lambda function step-by-step. + +### Additional Resources + +- [Core Concepts](core-concepts.md) – Understand the framework architecture +- [Project Structure](project-structure.md) – Learn how to organize your code +- [Examples](/examples/) – Browse complete working examples +- [API Reference](/api-reference/) – Detailed API documentation diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md new file mode 100644 index 00000000..826711b1 --- /dev/null +++ b/docs/getting-started/project-structure.md @@ -0,0 +1,677 @@ +# Project Structure + +As your Lambda function grows from a simple handler to a production service, proper organization becomes essential. This guide shows you how to structure Lambda projects for maintainability, testability, and scalability. + +## Introduction + +Good project structure helps you: + +- **Find code quickly** – Organized by responsibility +- **Test effectively** – Clear separation of concerns +- **Scale smoothly** – Easy to add new features +- **Maintain easily** – Consistent patterns + +We'll progress from simple to complex structures, so you can choose what fits your needs. + +## Simple Lambda Structure + +For straightforward Lambdas with a single responsibility: + +``` +MyLambda/ +├── MyLambda.csproj # Project file +├── Program.cs # Entry point + handler +├── Models/ +│ ├── Request.cs # Input model +│ └── Response.cs # Output model +├── Services/ +│ ├── IMyService.cs # Service interface +│ └── MyService.cs # Service implementation +└── appsettings.json # Configuration (optional) +``` + +**When to use:** +- Single event source +- Simple business logic +- 1-2 services +- Under 500 lines of code + +## Modular Service Structure + +For complex Lambdas with multiple services and responsibilities: + +``` +OrderProcessingLambda/ +├── OrderProcessingLambda.csproj +├── Program.cs # Entry point + DI registration +├── Models/ +│ ├── Order.cs +│ ├── OrderItem.cs +│ ├── OrderRequest.cs +│ └── OrderResponse.cs +├── Services/ +│ ├── IOrderService.cs +│ ├── OrderService.cs +│ ├── IInventoryService.cs +│ ├── InventoryService.cs +│ ├── IPaymentService.cs +│ └── PaymentService.cs +├── Repositories/ +│ ├── IOrderRepository.cs +│ └── OrderRepository.cs +├── Middleware/ +│ └── ValidationMiddleware.cs +├── Configuration/ +│ └── OrderProcessingOptions.cs +└── appsettings.json +``` + +**When to use:** +- Multiple services +- Complex business logic +- Data access layers +- Reusable middleware +- Configuration management + +## Program.cs Organization + +Your `Program.cs` file is the entry point for your Lambda. Following a consistent organization pattern makes it easier to understand and maintain. + +### Recommended Order + +```csharp title="Program.cs" linenums="1" +// 1. Using statements (explicit, grouped logically) +using System; +using System.Threading; +using System.Threading.Tasks; +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; +using MyLambda.Services; +using MyLambda.Models; + +// 2. Create builder +var builder = LambdaApplication.CreateBuilder(); + +// 3. Configure options (if needed) +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing") +); + +// 4. Register services (grouped by lifetime) +// Singletons first (shared across invocations) +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Scoped services (per invocation) +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// 5. Lifecycle hooks +builder.Services.AddOnInit(async (services, ct) => +{ + // Initialization logic +}); + +builder.Services.AddOnShutdown(async (services, ct) => +{ + // Cleanup logic +}); + +// 6. Build application +var lambda = builder.Build(); + +// 7. Register middleware (in order of execution) +lambda.UseMiddleware(); +lambda.UseMiddleware(); + +// 8. Register handler +lambda.MapHandler(([Event] OrderRequest request, IOrderService service) => + service.ProcessAsync(request) +); + +// 9. Run +await lambda.RunAsync(); +``` + +!!! tip "Consistent Organization" + Following this order consistently across your Lambda functions makes them easier to understand and maintain. + +## Extracting Services into Files + +As your `Program.cs` grows, extract services into separate files. + +### Before (Inline) + +```csharp title="Program.cs" +// Everything in one file +public interface IGreetingService +{ + string GetGreeting(string name); +} + +public class GreetingService : IGreetingService +{ + public string GetGreeting(string name) => $"Hello, {name}!"; +} + +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddSingleton(); +// ... +``` + +**Problems:** +- Hard to test in isolation +- File becomes very long +- Difficult to reuse +- Poor separation of concerns + +### After (Extracted) + +```csharp title="Services/IGreetingService.cs" +namespace MyLambda.Services; + +public interface IGreetingService +{ + string GetGreeting(string name); +} +``` + +```csharp title="Services/GreetingService.cs" +namespace MyLambda.Services; + +public class GreetingService : IGreetingService +{ + public string GetGreeting(string name) => $"Hello, {name}!"; +} +``` + +```csharp title="Program.cs" +using MyLambda.Services; + +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddSingleton(); +// ... +``` + +**Benefits:** +- Each file has one responsibility +- Easy to find and modify +- Testable in isolation +- Reusable across projects + +!!! tip "When to Extract" + Extract to separate files when: + - Service has more than 20 lines + - You need to test it in isolation + - It could be reused elsewhere + - Program.cs exceeds 100 lines + +## Test Project Structure + +Organize tests to mirror your source code structure: + +``` +MyLambda.Tests/ +├── MyLambda.Tests.csproj +├── Services/ +│ ├── OrderServiceTests.cs +│ └── InventoryServiceTests.cs +├── Handlers/ +│ └── OrderHandlerTests.cs +├── Middleware/ +│ └── ValidationMiddlewareTests.cs +├── Fixtures/ +│ ├── TestFixture.cs # Shared test setup +│ └── TestData.cs # Test data builders +└── Integration/ + └── LambdaIntegrationTests.cs +``` + +### Unit Test Example + +```csharp title="Services/OrderServiceTests.cs" +using Xunit; +using NSubstitute; +using MyLambda.Services; +using MyLambda.Models; + +public class OrderServiceTests +{ + [Fact] + public async Task ProcessAsync_ValidOrder_ReturnsSuccess() + { + // Arrange + var repository = Substitute.For(); + repository.SaveAsync(Arg.Any()) + .Returns(new SaveResult { Success = true, Id = "123" }); + + var service = new OrderService(repository); + var order = new Order("123", 99.99m); + + // Act + var result = await service.ProcessAsync(order); + + // Assert + Assert.True(result.Success); + Assert.Equal("123", result.OrderId); + await repository.Received(1).SaveAsync(order); + } + + [Fact] + public async Task ProcessAsync_InvalidOrder_ThrowsValidationException() + { + // Arrange + var repository = Substitute.For(); + var service = new OrderService(repository); + var order = new Order("", -1m); // Invalid order + + // Act & Assert + await Assert.ThrowsAsync(() => + service.ProcessAsync(order) + ); + } +} +``` + +!!! info "Testing Framework" + This project uses xUnit with NSubstitute for mocking. Adjust patterns to match your testing framework of choice. + +## Configuration Management + +Manage configuration externally rather than hardcoding values. + +### appsettings.json + +```json title="appsettings.json" +{ + "OrderProcessing": { + "MaxRetries": 3, + "TimeoutSeconds": 30, + "EnableCaching": true + }, + "Database": { + "ConnectionString": "Server=localhost;Database=orders", + "CommandTimeout": 30 + }, + "ExternalApi": { + "BaseUrl": "https://api.example.com", + "ApiKey": "" // Set via environment variable + } +} +``` + +### Options Class + +Create strongly-typed configuration classes: + +```csharp title="Configuration/OrderProcessingOptions.cs" +namespace MyLambda.Configuration; + +public class OrderProcessingOptions +{ + public int MaxRetries { get; init; } + public int TimeoutSeconds { get; init; } + public bool EnableCaching { get; init; } +} +``` + +### Binding Configuration + +Bind configuration sections to options classes: + +```csharp title="Program.cs" +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing") +); +``` + +### Using Options in Services + +Inject `IOptions` into your services: + +```csharp title="Services/OrderService.cs" +using Microsoft.Extensions.Options; + +public class OrderService : IOrderService +{ + private readonly OrderProcessingOptions _options; + private readonly IOrderRepository _repository; + + public OrderService( + IOptions options, + IOrderRepository repository) + { + _options = options.Value; + _repository = repository; + } + + public async Task ProcessAsync(Order order) + { + // Use configuration + if (_options.EnableCaching) + { + // Check cache... + } + + for (int retry = 0; retry < _options.MaxRetries; retry++) + { + try + { + return await _repository.SaveAsync(order); + } + catch (Exception ex) when (retry < _options.MaxRetries - 1) + { + // Retry logic + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + + throw new Exception("Max retries exceeded"); + } +} +``` + +## Environment-Specific Configuration + +Use multiple configuration files for different environments: + +### File Structure + +``` +MyLambda/ +├── appsettings.json # Base configuration +├── appsettings.Development.json # Development overrides +└── appsettings.Production.json # Production overrides +``` + +### Configuration Loading + +```csharp title="Program.cs" +var builder = LambdaApplication.CreateBuilder(); + +// Configuration is automatically loaded in this order: +// 1. appsettings.json (base) +// 2. appsettings.{Environment}.json (environment-specific) +// 3. Environment variables (highest priority) + +// Add additional configuration sources +builder.Configuration.AddEnvironmentVariables(); +``` + +### Environment Variables + +Access environment variables directly or through configuration: + +```csharp +// Direct access +var apiKey = Environment.GetEnvironmentVariable("API_KEY"); + +// Through configuration (recommended) +var apiKey = builder.Configuration["ExternalApi:ApiKey"]; +``` + +!!! warning "Never Commit Secrets" + Never commit API keys, connection strings, or other secrets to source control. Use environment variables or AWS Secrets Manager. + +## Secrets Management + +### AWS Secrets Manager Integration + +```csharp title="Program.cs" +using Amazon; +using Amazon.Extensions.Configuration.SystemsManager; + +var builder = LambdaApplication.CreateBuilder(); + +// Add AWS Secrets Manager as configuration source +builder.Configuration.AddSecretsManager( + region: RegionEndpoint.USEast1, + configurator: options => + { + options.SecretFilter = entry => entry.Name.StartsWith("MyLambda/"); + options.PollingInterval = TimeSpan.FromMinutes(5); + } +); +``` + +### Environment Variables Pattern + +Set sensitive values via Lambda environment variables: + +```csharp +// Lambda environment variable: DATABASE_CONNECTION_STRING +var connectionString = builder.Configuration["DATABASE_CONNECTION_STRING"]; + +// Or through options binding +public class DatabaseOptions +{ + public string ConnectionString { get; init; } = ""; +} + +builder.Services.Configure( + builder.Configuration.GetSection("Database") +); +``` + +## Deployment Structure + +### SAM Template Organization + +``` +MyLambda/ +├── src/ +│ └── MyLambda/ # Lambda source code +│ ├── Program.cs +│ └── MyLambda.csproj +├── test/ +│ └── MyLambda.Tests/ # Unit tests +├── template.yaml # SAM template +├── samconfig.toml # SAM configuration +└── events/ + ├── order-event.json # Test event 1 + └── error-event.json # Test event 2 +``` + +### CDK Project Structure + +``` +MyLambdaStack/ +├── src/ +│ ├── MyLambdaStack/ # CDK infrastructure code +│ │ ├── MyLambdaStack.cs +│ │ └── MyLambdaStack.csproj +│ └── MyLambda/ # Lambda function code +│ ├── Program.cs +│ └── MyLambda.csproj +├── test/ +│ └── MyLambdaStack.Tests/ +├── cdk.json +└── README.md +``` + +## Anti-Patterns to Avoid + +### ❌ Don't: Mix Concerns in Program.cs + +```csharp +// BAD: Business logic directly in Program.cs +lambda.MapHandler(([Event] Order order) => +{ + // 50+ lines of business logic + // Database queries + // External API calls + // Complex calculations + // Validation logic + return new OrderResponse(/* ... */); +}); +``` + +### ✅ Do: Extract to Services + +```csharp +// GOOD: Delegate to service +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.ProcessAsync(order) +); +``` + +--- + +### ❌ Don't: Register Everything as Singleton + +```csharp +// BAD: Wrong lifetime +builder.Services.AddSingleton(); +// Repository with per-request state should be Scoped! +``` + +### ✅ Do: Use Appropriate Lifetimes + +```csharp +// GOOD: Correct lifetime +builder.Services.AddSingleton(); // Stateless, shared +builder.Services.AddScoped(); // Per-request state +``` + +--- + +### ❌ Don't: Hardcode Configuration + +```csharp +// BAD: Hardcoded values +public class OrderService +{ + private const int MaxRetries = 3; + private const string ApiUrl = "https://api.example.com"; + private const int Timeout = 30; +} +``` + +### ✅ Do: Use Configuration Options + +```csharp +// GOOD: Configuration-driven +public class OrderService +{ + private readonly OrderProcessingOptions _options; + + public OrderService(IOptions options) + { + _options = options.Value; + } +} +``` + +--- + +### ❌ Don't: Put Models Everywhere + +```csharp +// BAD: Models mixed with logic +public class OrderService +{ + public record OrderRequest(string Id); // Don't define here + public record OrderResponse(bool Success); // Don't define here + + public OrderResponse Process(OrderRequest request) { ... } +} +``` + +### ✅ Do: Organize Models in Dedicated Folder + +```csharp +// GOOD: Models in Models/ folder +// Models/OrderRequest.cs +namespace MyLambda.Models; +public record OrderRequest(string Id, decimal Amount); + +// Models/OrderResponse.cs +namespace MyLambda.Models; +public record OrderResponse(string OrderId, bool Success); +``` + +--- + +### ❌ Don't: Use Magic Strings + +```csharp +// BAD: Magic strings +var connectionString = builder.Configuration["ConnectionStrings:Default"]; +var timeout = int.Parse(builder.Configuration["Timeout"]); +``` + +### ✅ Do: Use Strongly-Typed Options + +```csharp +// GOOD: Strongly-typed configuration +public class DatabaseOptions +{ + public string ConnectionString { get; init; } = ""; + public int CommandTimeout { get; init; } +} + +builder.Services.Configure( + builder.Configuration.GetSection("Database") +); +``` + +--- + +### ❌ Don't: Ignore Async/Await + +```csharp +// BAD: Blocking async code +lambda.MapHandler(([Event] Order order, IOrderService service) => +{ + var result = service.ProcessAsync(order).Result; // DON'T! + return result; +}); +``` + +### ✅ Do: Use Async/Await Properly + +```csharp +// GOOD: Proper async/await +lambda.MapHandler(async ([Event] Order order, IOrderService service) => +{ + var result = await service.ProcessAsync(order); + return result; +}); +``` + +## Key Takeaways + +1. **Start Simple**: Single file for simple Lambdas, expand as complexity grows +2. **Extract Early**: Move services to separate files before they become unwieldy +3. **Organize by Lifetime**: Group service registrations by Singleton vs Scoped +4. **Test Structure Mirrors Source**: Keep test organization consistent with source code +5. **Configuration Over Code**: Use `appsettings.json` and options pattern +6. **Secrets External**: Never commit secrets; use environment variables or AWS Secrets Manager +7. **Consistent Ordering**: Follow Program.cs organization pattern across all Lambdas +8. **Avoid Anti-Patterns**: Don't mix concerns, hardcode values, or use wrong lifetimes + +## Next Steps + +You now understand how to structure Lambda projects for maintainability and scalability. + +### Continue Learning + +- **[Middleware Patterns](/guides/middleware.md)** – Build reusable middleware components +- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns +- **[Testing Strategies](/guides/testing.md)** – Comprehensive testing approaches +- **[Deployment Best Practices](/guides/deployment.md)** – CI/CD and production deployments + +### Explore Features + +- **[Envelopes](/features/envelopes/)** – Type-safe event source integration +- **[OpenTelemetry](/features/opentelemetry.md)** – Add distributed tracing +- **[AOT Compilation](/advanced/aot-compilation.md)** – Optimize for fastest cold starts + +### Browse Examples + +- **[Examples](/examples/)** – Complete working examples +- **[API Reference](/api-reference/)** – Detailed API documentation + +--- + +Congratulations! You've completed the Getting Started guide. You now have the knowledge to build, organize, and deploy production-ready Lambda functions with aws-lambda-host. diff --git a/mkdocs.yml b/mkdocs.yml index d8e11a48..efd77ccd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,7 +48,11 @@ markdown_extensions: pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true - pymdownx.details @@ -58,7 +62,13 @@ watch: - ./docs/ nav: - - Homepage : index.md + - Home: index.md + - Getting Started: + - getting-started/index.md + - Installation: getting-started/installation.md + - Your First Lambda: getting-started/first-lambda.md + - Core Concepts: getting-started/core-concepts.md + - Project Structure: getting-started/project-structure.md plugins: - search \ No newline at end of file From d39fe54aaf5fa79339bcd52f9b0b977568feba8e Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 08:53:31 -0500 Subject: [PATCH 02/65] feat(docs): add guides for DI, lifecycle management, and references - Added detailed guide on dependency injection with examples and best practices. - Documented lifecycle management including OnInit, Invocation, and OnShutdown handlers. - Created a comprehensive index for accessing all framework guides. - Included troubleshooting, anti-patterns, and advanced configuration examples. --- docs/guides/dependency-injection.md | 897 +++++++++++++++++++++++++++ docs/guides/index.md | 141 +++++ docs/guides/lifecycle-management.md | 861 ++++++++++++++++++++++++++ docs/guides/middleware.md | 914 ++++++++++++++++++++++++++++ 4 files changed, 2813 insertions(+) create mode 100644 docs/guides/dependency-injection.md create mode 100644 docs/guides/index.md create mode 100644 docs/guides/lifecycle-management.md create mode 100644 docs/guides/middleware.md diff --git a/docs/guides/dependency-injection.md b/docs/guides/dependency-injection.md new file mode 100644 index 00000000..d99d5034 --- /dev/null +++ b/docs/guides/dependency-injection.md @@ -0,0 +1,897 @@ +# Dependency Injection + +Dependency injection (DI) is central to building maintainable Lambda functions with aws-lambda-host. The framework integrates Microsoft.Extensions.DependencyInjection, providing the same DI patterns you use in ASP.NET Core—but optimized for Lambda's unique execution model. + +## Introduction + +In Lambda functions, understanding service lifetimes is critical: + +- **Singleton services** persist across invocations (cold start to shutdown) +- **Scoped services** are created fresh for each invocation +- **Transient services** are created each time they're requested + +This guide teaches you how to leverage DI effectively in Lambda functions. + +## Service Lifetimes + +### Singleton Services + +Singleton services are created once during the first invocation and reused for the lifetime of the Lambda execution environment. + +**When to use:** +- Stateless services +- HTTP clients (with connection pooling) +- Cache instances +- Configuration objects +- Database connection pools + +```csharp title="Program.cs" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +// Singleton: Created once, reused across all invocations +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var lambda = builder.Build(); +``` + +**Benefits:** +- ✅ Optimal performance (no repeated initialization) +- ✅ Shared state across invocations (caching, connection pools) +- ✅ Lower memory usage + +**⚠️ Warning:** Singleton services must be thread-safe since Lambda can process concurrent invocations in the same execution environment. + +### Scoped Services + +Scoped services are created once per Lambda invocation and disposed when the invocation completes. + +**When to use:** +- Services with per-request state +- Database contexts (Entity Framework) +- Unit of Work patterns +- Request-specific logging contexts +- Services that shouldn't be shared between requests + +```csharp title="Program.cs" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +// Scoped: New instance per invocation +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var lambda = builder.Build(); +``` + +**Benefits:** +- ✅ Isolated per invocation (no cross-request contamination) +- ✅ Automatic disposal after each request +- ✅ Safe for services with mutable state + +### Transient Services + +Transient services are created each time they're requested from the service provider. + +**When to use:** +- Lightweight, stateless services +- Services that need fresh instances each time +- Rarely needed in Lambda (prefer Scoped for most use cases) + +```csharp title="Program.cs" +builder.Services.AddTransient(); +``` + +**⚠️ Note:** Transient services are rarely necessary in Lambda. Prefer Scoped for per-invocation services and Singleton for shared services. + +## Service Registration Patterns + +### Basic Registration + +Register interface/implementation pairs: + +```csharp title="Program.cs" +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddTransient(); +``` + +### Concrete Types + +Register concrete types without interfaces: + +```csharp title="Program.cs" +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +``` + +### Factory Registration + +Use factories for complex initialization: + +```csharp title="Program.cs" +builder.Services.AddSingleton(sp => +{ + var logger = sp.GetRequiredService>(); + return new MemoryCache( + new MemoryCacheOptions + { + SizeLimit = 1024, + CompactionPercentage = 0.25 + }, + logger + ); +}); +``` + +### Multiple Implementations + +Register multiple implementations of the same interface: + +```csharp title="Program.cs" +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Inject all implementations +lambda.MapHandler(([Event] Order order, IEnumerable notifiers) => +{ + foreach (var notifier in notifiers) + { + notifier.Notify(order); + } +}); +``` + +### Try Add + +Register services only if not already registered: + +```csharp title="Program.cs" +builder.Services.TryAddSingleton(); +// If ICache is already registered, this does nothing +``` + +### Replace Services + +Replace existing service registrations: + +```csharp title="Program.cs" +builder.Services.Replace( + ServiceDescriptor.Singleton() +); +``` + +## Dependency Injection in Handlers + +Handlers can inject any registered service, plus framework-provided types. + +### Injectable Types + +The framework automatically injects these types into handlers: + +| Type | Description | Lifetime | +|------|-------------|----------| +| `[Event] T` | The Lambda event payload (deserialized) | Per invocation | +| `IServiceType` | Any registered service | As registered | +| `ILambdaHostContext` | Lambda invocation context | Per invocation | +| `CancellationToken` | Cancellation signal (timeout tracking) | Per invocation | + +### Basic Handler Injection + +```csharp title="Program.cs" +lambda.MapHandler(([Event] OrderRequest request, IOrderService service) => + service.ProcessAsync(request) +); +``` + +### Multiple Dependencies + +Inject multiple services into handlers: + +```csharp title="Program.cs" +lambda.MapHandler(async ( + [Event] OrderRequest request, + IOrderService orderService, + IInventoryService inventoryService, + IPaymentService paymentService, + ILogger logger +) => +{ + logger.LogInformation("Processing order {OrderId}", request.OrderId); + + var inventoryAvailable = await inventoryService.CheckAsync(request.Items); + if (!inventoryAvailable) + { + return new OrderResponse { Success = false, Reason = "Inventory unavailable" }; + } + + var paymentResult = await paymentService.ChargeAsync(request.Payment); + if (!paymentResult.Success) + { + return new OrderResponse { Success = false, Reason = "Payment failed" }; + } + + var orderResult = await orderService.CreateAsync(request); + return new OrderResponse { Success = true, OrderId = orderResult.OrderId }; +}); +``` + +### Injecting Context + +Use `ILambdaHostContext` to access invocation metadata: + +```csharp title="Program.cs" +lambda.MapHandler(async ( + [Event] OrderRequest request, + IOrderService service, + ILambdaHostContext context +) => +{ + // Access scoped service provider + var cache = context.ServiceProvider.GetRequiredService(); + + // Access cancellation token + if (context.CancellationToken.IsCancellationRequested) + { + return new OrderResponse { Success = false, Reason = "Timeout" }; + } + + // Store invocation-scoped data + context.Items["StartTime"] = DateTimeOffset.UtcNow; + + return await service.ProcessAsync(request, context.CancellationToken); +}); +``` + +### Injecting CancellationToken + +Use `CancellationToken` to handle Lambda timeouts gracefully: + +```csharp title="Program.cs" +lambda.MapHandler(async ( + [Event] OrderRequest request, + IOrderService service, + CancellationToken cancellationToken +) => +{ + try + { + return await service.ProcessAsync(request, cancellationToken); + } + catch (OperationCanceledException) + { + // Lambda is approaching timeout + return new OrderResponse { Success = false, Reason = "Timeout" }; + } +}); +``` + +## Dependency Injection in Middleware + +Middleware can resolve services from the DI container using the `ILambdaHostContext`. + +### Service Resolution in Middleware + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + var logger = context.ServiceProvider.GetRequiredService>(); + var stopwatch = Stopwatch.StartNew(); + + logger.LogInformation("Request starting"); + + await next(context); + + stopwatch.Stop(); + logger.LogInformation("Request completed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); +}); +``` + +### Middleware with Constructor Injection + +Create reusable middleware classes with constructor injection: + +```csharp title="Middleware/LoggingMiddleware.cs" +using AwsLambda.Host.Abstractions; +using Microsoft.Extensions.Logging; + +public class LoggingMiddleware +{ + private readonly ILogger _logger; + + public LoggingMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) + { + _logger.LogInformation("Starting invocation"); + await next(context); + _logger.LogInformation("Invocation completed"); + } +} +``` + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +var lambda = builder.Build(); +lambda.UseMiddleware(); +``` + +## Dependency Injection in Lifecycle Hooks + +Both `OnInit` and `OnShutdown` handlers support dependency injection. + +### OnInit with DI + +```csharp title="Program.cs" +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + var cache = services.GetRequiredService(); + var logger = services.GetRequiredService>(); + + logger.LogInformation("Warming up cache"); + await cache.WarmUpAsync(ct); + + return true; // Continue initialization +}); +``` + +### OnShutdown with DI + +```csharp title="Program.cs" +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + var cache = services.GetRequiredService(); + var logger = services.GetRequiredService>(); + + logger.LogInformation("Flushing cache"); + await cache.FlushAsync(ct); +}); +``` + +## Configuration with Options Pattern + +Use the options pattern to inject strongly-typed configuration. + +### Defining Options Classes + +```csharp title="Configuration/OrderProcessingOptions.cs" +namespace MyLambda.Configuration; + +public class OrderProcessingOptions +{ + public int MaxRetries { get; init; } + public int TimeoutSeconds { get; init; } + public bool EnableCaching { get; init; } +} +``` + +### Binding Configuration + +```csharp title="Program.cs" +using Microsoft.Extensions.Options; + +var builder = LambdaApplication.CreateBuilder(); + +// Bind configuration section to options class +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing") +); + +builder.Services.AddScoped(); + +var lambda = builder.Build(); +``` + +### appsettings.json + +```json title="appsettings.json" +{ + "OrderProcessing": { + "MaxRetries": 3, + "TimeoutSeconds": 30, + "EnableCaching": true + } +} +``` + +### Injecting Options + +```csharp title="Services/OrderService.cs" +using Microsoft.Extensions.Options; + +public class OrderService : IOrderService +{ + private readonly OrderProcessingOptions _options; + private readonly IOrderRepository _repository; + + public OrderService( + IOptions options, + IOrderRepository repository) + { + _options = options.Value; + _repository = repository; + } + + public async Task ProcessAsync(Order order) + { + if (_options.EnableCaching) + { + // Check cache + } + + for (int retry = 0; retry < _options.MaxRetries; retry++) + { + try + { + return await _repository.SaveAsync(order); + } + catch (Exception) when (retry < _options.MaxRetries - 1) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + + throw new Exception("Max retries exceeded"); + } +} +``` + +### Options Variants + +```csharp title="Program.cs" +// IOptions - Singleton, value never reloads +builder.Services.AddScoped(); + +// IOptionsSnapshot - Scoped, reloads per invocation +builder.Services.AddScoped(); + +// IOptionsMonitor - Singleton, reloads when config changes +builder.Services.AddSingleton(); +``` + +**For Lambda, prefer `IOptions`** since configuration typically doesn't change during function execution. + +## Best Practices + +### ✅ Do: Use Scoped for Per-Invocation State + +```csharp +// GOOD: Repository with per-invocation state +builder.Services.AddScoped(); +``` + +### ❌ Don't: Use Singleton for Stateful Services + +```csharp +// BAD: Singleton with mutable state causes cross-invocation contamination +builder.Services.AddSingleton(); +``` + +### ✅ Do: Use Singleton for Shared Resources + +```csharp +// GOOD: HTTP client pool is thread-safe and benefits from reuse +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +``` + +### ✅ Do: Register Interfaces, Not Implementations + +```csharp +// GOOD: Testable and flexible +builder.Services.AddScoped(); + +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.ProcessAsync(order) +); +``` + +### ❌ Don't: Use Concrete Types Everywhere + +```csharp +// BAD: Hard to test and tightly coupled +builder.Services.AddScoped(); + +lambda.MapHandler(([Event] Order order, OrderService service) => + service.ProcessAsync(order) +); +``` + +### ✅ Do: Use Options Pattern for Configuration + +```csharp +// GOOD: Strongly-typed, testable configuration +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing") +); +``` + +### ❌ Don't: Hardcode Configuration Values + +```csharp +// BAD: Hardcoded, difficult to test and change +public class OrderService +{ + private const int MaxRetries = 3; + private const string ApiUrl = "https://api.example.com"; +} +``` + +### ✅ Do: Dispose Resources Properly + +```csharp +// GOOD: Scoped services are automatically disposed after each invocation +builder.Services.AddScoped(); + +public class OrderRepository : IOrderRepository, IDisposable +{ + private readonly HttpClient _httpClient; + + public void Dispose() + { + _httpClient?.Dispose(); + } +} +``` + +### ✅ Do: Inject CancellationToken for Timeout Handling + +```csharp +// GOOD: Gracefully handle Lambda timeouts +lambda.MapHandler(async ( + [Event] Order order, + IOrderService service, + CancellationToken cancellationToken +) => +{ + return await service.ProcessAsync(order, cancellationToken); +}); +``` + +## Anti-Patterns to Avoid + +### ❌ Service Locator Pattern + +```csharp +// BAD: Service locator anti-pattern +lambda.MapHandler(([Event] Order order, IServiceProvider services) => +{ + var service = services.GetRequiredService(); + return service.ProcessAsync(order); +}); +``` + +**Why it's bad:** +- Hides dependencies (hard to test) +- Runtime errors instead of compile-time errors +- Violates dependency inversion principle + +**Better approach:** + +```csharp +// GOOD: Explicit dependency injection +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.ProcessAsync(order) +); +``` + +--- + +### ❌ Registering Everything as Singleton + +```csharp +// BAD: All services are Singleton +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +``` + +**Why it's bad:** +- Cross-invocation state contamination +- Memory leaks (services never disposed) +- Thread-safety issues + +**Better approach:** + +```csharp +// GOOD: Use appropriate lifetimes +builder.Services.AddSingleton(); // Stateless, shared +builder.Services.AddScoped(); // Per-invocation +builder.Services.AddScoped(); // Per-invocation +``` + +--- + +### ❌ Constructor Over-Injection + +```csharp +// BAD: Too many dependencies +public class OrderService +{ + public OrderService( + IOrderRepository orderRepo, + IInventoryService inventoryService, + IPaymentService paymentService, + INotificationService notificationService, + ILogger logger, + IMetricsCollector metrics, + ICache cache, + IValidator validator, + IMapper mapper + ) + { + // ... + } +} +``` + +**Why it's bad:** +- Service Responsibility Principle violation +- Hard to test and maintain +- Usually indicates poor design + +**Better approach:** + +```csharp +// GOOD: Split into smaller services +public class OrderService +{ + public OrderService( + IOrderRepository repository, + IOrderProcessor processor, + ILogger logger + ) + { + // ... + } +} + +public class OrderProcessor +{ + public OrderProcessor( + IInventoryService inventory, + IPaymentService payment, + INotificationService notification + ) + { + // ... + } +} +``` + +--- + +### ❌ Mixing Lifetimes Incorrectly + +```csharp +// BAD: Singleton depends on Scoped service +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +public class OrderCache : IOrderCache +{ + public OrderCache(IOrderRepository repository) // PROBLEM! + { + // Singleton capturing Scoped dependency + } +} +``` + +**Why it's bad:** +- Scoped service is captured by Singleton +- First invocation's scoped instance is reused forever +- Cross-invocation state contamination + +**Better approach:** + +```csharp +// GOOD: Inject IServiceProvider and resolve per-invocation +public class OrderCache : IOrderCache +{ + private readonly IServiceProvider _serviceProvider; + + public OrderCache(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task GetAsync(string id) + { + using var scope = _serviceProvider.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + return await repository.GetAsync(id); + } +} +``` + +## Common Patterns + +### Repository Pattern + +```csharp title="Program.cs" +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var lambda = builder.Build(); + +lambda.MapHandler(([Event] OrderRequest request, IOrderService service) => + service.ProcessAsync(request) +); +``` + +```csharp title="Services/OrderService.cs" +public class OrderService : IOrderService +{ + private readonly IOrderRepository _repository; + + public OrderService(IOrderRepository repository) + { + _repository = repository; + } + + public async Task ProcessAsync(OrderRequest request) + { + var order = new Order(request.Id, request.Amount); + await _repository.SaveAsync(order); + return new OrderResponse(order.Id, true); + } +} +``` + +### Unit of Work Pattern + +```csharp title="Program.cs" +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => + sp.GetRequiredService().Orders +); + +var lambda = builder.Build(); +``` + +```csharp title="Data/UnitOfWork.cs" +public class UnitOfWork : IUnitOfWork, IDisposable +{ + private readonly DbContext _context; + + public UnitOfWork(DbContext context) + { + _context = context; + Orders = new OrderRepository(context); + } + + public IOrderRepository Orders { get; } + + public async Task CommitAsync() => await _context.SaveChangesAsync(); + + public void Dispose() => _context?.Dispose(); +} +``` + +### Decorator Pattern + +```csharp title="Program.cs" +builder.Services.AddScoped(); +builder.Services.Decorate(); + +var lambda = builder.Build(); +``` + +```csharp title="Services/CachedOrderService.cs" +public class CachedOrderService : IOrderService +{ + private readonly IOrderService _inner; + private readonly ICache _cache; + + public CachedOrderService(IOrderService inner, ICache cache) + { + _inner = inner; + _cache = cache; + } + + public async Task ProcessAsync(OrderRequest request) + { + var cacheKey = $"order:{request.Id}"; + if (_cache.TryGet(cacheKey, out OrderResponse cached)) + { + return cached; + } + + var result = await _inner.ProcessAsync(request); + _cache.Set(cacheKey, result); + return result; + } +} +``` + +## Troubleshooting + +### Service Not Found + +**Error:** + +``` +InvalidOperationException: No service for type 'IOrderService' has been registered. +``` + +**Solution:** + +Ensure the service is registered before building: + +```csharp +builder.Services.AddScoped(); +var lambda = builder.Build(); +``` + +### Singleton Capturing Scoped Dependency + +**Problem:** Singleton service depends on Scoped service. + +**Solution:** Use `IServiceProvider` to resolve Scoped services dynamically: + +```csharp +public class MySingletonService +{ + private readonly IServiceProvider _serviceProvider; + + public MySingletonService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task DoWorkAsync() + { + using var scope = _serviceProvider.CreateScope(); + var scopedService = scope.ServiceProvider.GetRequiredService(); + await scopedService.DoWorkAsync(); + } +} +``` + +### Disposed Service Access + +**Error:** + +``` +ObjectDisposedException: Cannot access a disposed object. +``` + +**Problem:** Attempting to use a Scoped service after the invocation completes. + +**Solution:** Ensure services are only used within the invocation scope. Don't store Scoped services in Singleton fields. + +## Key Takeaways + +1. **Understand Lifetimes**: Use Singleton for stateless shared resources, Scoped for per-invocation services +2. **Inject Dependencies**: Use constructor injection, not service locator pattern +3. **Options Pattern**: Use `IOptions` for strongly-typed configuration +4. **CancellationToken**: Always inject `CancellationToken` for timeout handling +5. **Avoid Anti-Patterns**: Don't mix lifetimes incorrectly or over-inject dependencies +6. **Reusable Middleware**: Create middleware classes with constructor injection +7. **Lifecycle DI**: Use `IServiceProvider` in `OnInit` and `OnShutdown` handlers + +## Next Steps + +Now that you understand dependency injection, explore related topics: + +- **[Middleware](/guides/middleware.md)** – Build middleware pipelines with DI +- **[Lifecycle Management](/guides/lifecycle-management.md)** – Use DI in OnInit and OnShutdown +- **[Configuration](/guides/configuration.md)** – Advanced configuration patterns +- **[Testing](/guides/testing.md)** – Test services with dependency injection +- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns with DI + +--- + +Congratulations! You now understand how to leverage dependency injection to build maintainable, testable Lambda functions. diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 00000000..6abe3a84 --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,141 @@ +# Guides + +Comprehensive guides for building production Lambda functions with aws-lambda-host. Each guide provides in-depth coverage of a specific framework feature with complete examples, best practices, and troubleshooting. + +## Core Framework Guides + +Master the essential framework features that power your Lambda functions. + +### [Dependency Injection](dependency-injection.md) +Learn service registration patterns, understand Singleton vs Scoped lifetimes, and master dependency injection in handlers and lifecycle methods. + +**Topics covered:** +- Service lifetime management (Singleton, Scoped) +- Service registration patterns +- Injectable parameter types +- Options pattern and configuration +- Best practices and anti-patterns + +### [Middleware](middleware.md) +Build middleware pipelines for cross-cutting concerns like logging, metrics, validation, and error handling. + +**Topics covered:** +- Middleware pipeline composition +- Common middleware patterns +- Context and state management +- Execution order and control flow +- Reusable middleware components + +### [Lifecycle Management](lifecycle-management.md) +Understand and control the Lambda lifecycle phases: OnInit, Invocation, and OnShutdown. + +**Topics covered:** +- OnInit phase for cold start setup +- OnShutdown phase for cleanup +- Multiple lifecycle handlers +- Timeout configuration +- Error handling in lifecycle + +### [Handler Registration](handler-registration.md) +Register type-safe Lambda handlers with automatic dependency injection and source generation. + +**Topics covered:** +- MapHandler method usage +- `[Event]` attribute requirements +- Injectable parameter types +- Return type handling +- Source generation benefits +- Handler patterns + +### [Configuration](configuration.md) +Configure framework behavior with LambdaHostOptions and application settings. + +**Topics covered:** +- LambdaHostOptions reference +- Timeout and cancellation configuration +- Application configuration patterns +- JSON serialization options +- Environment variables +- Best practices + +## Development Guides + +Build robust, testable, and deployable Lambda functions. + +### [Error Handling](error-handling.md) +Implement resilient error handling with retries, graceful degradation, and proper exception management. + +**Topics covered:** +- Exception handling in handlers +- Middleware error handling +- Cancellation token usage +- Retry patterns and strategies +- Dead Letter Queue integration +- Best practices + +### [Testing](testing.md) +Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, and AutoFixture. + +**Topics covered:** +- Testing framework setup +- Unit testing services +- AutoNSubstituteData pattern +- Testing handlers and middleware +- Integration testing +- Test naming conventions + +### [Deployment](deployment.md) +Deploy Lambda functions using AWS SAM, CDK, Terraform, or CI/CD pipelines. + +**Topics covered:** +- Project configuration (standard and AOT) +- AWS SAM deployment +- AWS CDK deployment +- CI/CD with GitHub Actions +- Versioning and releases +- Best practices + +## Guide Features + +Each guide includes: + +- ✅ **Complete working examples** – Copy-paste ready code +- ✅ **Best practices** – Proven patterns from production use +- ✅ **Anti-patterns** – Common mistakes to avoid +- ✅ **Troubleshooting** – Solutions to common issues +- ✅ **Cross-references** – Links to related topics + +## Learning Path + +### New to aws-lambda-host? + +Start with [Getting Started](/getting-started/) to build your first Lambda function, then return here for deeper coverage of specific features. + +### Building Production Lambda Functions? + +Use these guides as reference documentation when implementing specific features. Each guide is self-contained and can be read independently. + +### Optimizing Performance? + +After mastering the guides, explore [Advanced Topics](/advanced/) for AOT compilation, source generators, and performance optimization. + +## Additional Resources + +- **[Examples](/examples/)** – Complete example projects +- **[Features](/features/)** – Envelope packages and OpenTelemetry +- **[API Reference](/api-reference/)** – Detailed API documentation +- **[FAQ](/resources/faq.md)** – Common questions and answers +- **[Troubleshooting](/resources/troubleshooting.md)** – Solutions to common problems + +## Getting Help + +If you encounter issues not covered in these guides: + +- Check the [FAQ](/resources/faq.md) for common questions +- Review [Troubleshooting](/resources/troubleshooting.md) for solutions +- Search or ask in [GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions) +- Report bugs in [GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues) + +--- + +Ready to dive in? Choose a guide above or start with [Dependency Injection](dependency-injection.md). diff --git a/docs/guides/lifecycle-management.md b/docs/guides/lifecycle-management.md new file mode 100644 index 00000000..b3d223bd --- /dev/null +++ b/docs/guides/lifecycle-management.md @@ -0,0 +1,861 @@ +# Lifecycle Management + +AWS Lambda functions have three distinct execution phases: **Init**, **Invocation**, and **Shutdown**. Understanding and leveraging these phases is key to building high-performance, resource-efficient Lambda functions. + +## Introduction + +Lambda execution follows this lifecycle: + +``` +Cold Start → OnInit → Invocation 1 → Invocation 2 → ... → OnShutdown → Termination +``` + +- **OnInit**: Runs once during cold start (function initialization) +- **Invocation**: Runs for each Lambda event +- **OnShutdown**: Runs once before Lambda container terminates + +aws-lambda-host provides explicit control over each phase through lifecycle handlers. + +## Lambda Lifecycle Phases + +### Phase 1: OnInit (Cold Start) + +The OnInit phase executes once when Lambda initializes a new execution environment. Use this phase for: + +- Warming up caches +- Establishing database connections +- Preloading configuration +- Initializing HTTP clients +- Loading machine learning models + +**Characteristics:** +- Runs **once per execution environment** +- Executes **before the first invocation** +- Shares execution time with the first invocation +- Multiple handlers execute **concurrently** + +### Phase 2: Invocation + +The invocation phase processes each incoming Lambda event. This is where your handler logic executes. + +**Characteristics:** +- Runs **for each event** +- Isolated scope per invocation +- Scoped services created fresh for each invocation + +### Phase 3: OnShutdown + +The OnShutdown phase executes once before Lambda terminates the execution environment. Use this phase for: + +- Flushing logs and metrics +- Closing database connections +- Cleaning up temporary resources +- Graceful shutdown of background tasks + +**Characteristics:** +- Runs **once before termination** +- Triggered by SIGTERM signal +- Limited time to complete (configurable) +- Multiple handlers execute **sequentially** + +## OnInit Handlers + +### Basic OnInit + +```csharp title="Program.cs" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("Initializing Lambda function..."); + + // Perform one-time setup + var cache = services.GetRequiredService(); + await cache.WarmUpAsync(ct); + + return true; // true = continue, false = abort initialization +}); + +lambda.MapHandler(([Event] Request request) => new Response("Success")); + +await lambda.RunAsync(); +``` + +**Return Value:** +- `true` – Continue initialization +- `false` – Abort initialization (Lambda reports failure) + +### Multiple OnInit Handlers + +Register multiple initialization handlers—they execute **concurrently**: + +```csharp title="Program.cs" +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("Warming up cache"); + var cache = services.GetRequiredService(); + await cache.WarmUpAsync(ct); + return true; +}); + +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("Initializing database connection pool"); + var db = services.GetRequiredService(); + await db.InitializeAsync(ct); + return true; +}); + +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("Preloading configuration"); + var config = services.GetRequiredService(); + await config.LoadAsync(ct); + return true; +}); +``` + +**Execution**: All three handlers run **concurrently** for faster cold starts. + +### OnInit with Dependency Injection + +OnInit handlers can inject any registered service: + +```csharp title="Program.cs" +lambda.OnInit(async ( + ICache cache, + IDatabase database, + ILogger logger, + CancellationToken ct +) => +{ + logger.LogInformation("Starting initialization"); + + await Task.WhenAll( + cache.WarmUpAsync(ct), + database.InitializeAsync(ct) + ); + + logger.LogInformation("Initialization complete"); + return true; +}); +``` + +### Handling OnInit Failures + +If any handler returns `false`, initialization aborts: + +```csharp title="Program.cs" +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + var config = services.GetRequiredService(); + + try + { + await config.LoadAsync(ct); + return true; // Success + } + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to load configuration: {ex.Message}"); + return false; // Abort initialization + } +}); +``` + +**Result**: Lambda reports initialization failure, preventing invocations. + +### OnInit Timeout + +Configure OnInit timeout using `LambdaHostOptions`: + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.InitTimeout = TimeSpan.FromSeconds(10); // Default: 5 seconds +}); + +var lambda = builder.Build(); + +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + // This cancellation token will fire after 10 seconds + try + { + await LongRunningInitializationAsync(ct); + return true; + } + catch (OperationCanceledException) + { + Console.Error.WriteLine("Initialization timed out"); + return false; + } +}); +``` + +## OnShutdown Handlers + +### Basic OnShutdown + +```csharp title="Program.cs" +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("Shutting down Lambda function..."); + + var cache = services.GetRequiredService(); + await cache.FlushAsync(ct); +}); + +lambda.MapHandler(([Event] Request request) => new Response("Success")); + +await lambda.RunAsync(); +``` + +**No return value required** – handlers execute and complete. + +### Multiple OnShutdown Handlers + +Register multiple shutdown handlers—they execute **sequentially**: + +```csharp title="Program.cs" +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("1. Flushing metrics"); + var metrics = services.GetRequiredService(); + await metrics.FlushAsync(ct); +}); + +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("2. Closing database connections"); + var db = services.GetRequiredService(); + await db.CloseAsync(ct); +}); + +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + Console.WriteLine("3. Cleanup complete"); +}); +``` + +**Execution**: Handlers run **sequentially** in registration order. + +### OnShutdown with Dependency Injection + +OnShutdown handlers support dependency injection: + +```csharp title="Program.cs" +lambda.OnShutdown(async ( + IMetrics metrics, + IDatabase database, + ILogger logger, + CancellationToken ct +) => +{ + logger.LogInformation("Starting shutdown"); + + // Sequential cleanup + await metrics.FlushAsync(ct); + await database.CloseAsync(ct); + + logger.LogInformation("Shutdown complete"); +}); +``` + +### Handling OnShutdown Errors + +OnShutdown errors are logged but don't prevent shutdown: + +```csharp title="Program.cs" +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + try + { + var cache = services.GetRequiredService(); + await cache.FlushAsync(ct); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Cache flush failed: {ex.Message}"); + // Shutdown continues despite error + } +}); +``` + +### OnShutdown Timeout + +Configure shutdown timeout using `LambdaHostOptions`: + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Time between SIGTERM and SIGKILL + options.ShutdownDuration = TimeSpan.FromMilliseconds(500); // Default: 500ms + + // Buffer to ensure completion before SIGKILL + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(50); // Default: 50ms +}); + +var lambda = builder.Build(); + +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + // Cancellation token fires after (500ms - 50ms = 450ms) + try + { + await GracefulShutdownAsync(ct); + } + catch (OperationCanceledException) + { + Console.Error.WriteLine("Shutdown timed out"); + } +}); +``` + +**Shutdown Duration Options:** + +```csharp +// No extension time (0ms) +options.ShutdownDuration = ShutdownDuration.NoExtensions; + +// Internal extensions only (300ms) +options.ShutdownDuration = ShutdownDuration.InternalExtensions; + +// External extensions (500ms) - DEFAULT +options.ShutdownDuration = ShutdownDuration.ExternalExtensions; + +// Custom duration +options.ShutdownDuration = TimeSpan.FromSeconds(2); +``` + +## Common Patterns + +### Warming Up Caches + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + Console.WriteLine("Warming up cache..."); + + // Preload frequently accessed data + await cache.SetAsync("config", await LoadConfigAsync(ct), ct); + await cache.SetAsync("lookup", await LoadLookupDataAsync(ct), ct); + + Console.WriteLine("Cache warmed"); + return true; +}); +``` + +### Database Connection Pooling + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +lambda.OnInit(async (IDatabase db, ILogger logger, CancellationToken ct) => +{ + logger.LogInformation("Initializing database connection pool"); + await db.InitializePoolAsync(ct); + return true; +}); + +lambda.OnShutdown(async (IDatabase db, ILogger logger, CancellationToken ct) => +{ + logger.LogInformation("Closing database connections"); + await db.ClosePoolAsync(ct); +}); +``` + +### Telemetry Flushing + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +lambda.OnShutdown(async (ITelemetry telemetry, ILogger logger, CancellationToken ct) => +{ + logger.LogInformation("Flushing telemetry"); + await telemetry.FlushAsync(ct); +}); +``` + +### Loading ML Models + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +lambda.OnInit(async (IModelService models, ILogger logger, CancellationToken ct) => +{ + logger.LogInformation("Loading ML model"); + await models.LoadAsync("model-v1.0", ct); + logger.LogInformation("Model loaded"); + return true; +}); +``` + +### Configuration Preloading + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +lambda.OnInit(async (IConfigService config, ILogger logger, CancellationToken ct) => +{ + logger.LogInformation("Preloading configuration from SSM"); + + try + { + await config.LoadFromParameterStoreAsync(ct); + logger.LogInformation("Configuration loaded successfully"); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to load configuration"); + return false; // Abort initialization + } +}); +``` + +## Lifecycle Configuration + +### LambdaHostOptions Reference + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // OnInit timeout (default: 5 seconds) + options.InitTimeout = TimeSpan.FromSeconds(10); + + // Invocation cancellation buffer (default: 3 seconds) + // Buffer before Lambda timeout to allow graceful cancellation + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); + + // Shutdown duration (default: 500ms) + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; + + // Shutdown buffer (default: 50ms) + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); + + // Clear Lambda output formatting (default: false) + options.ClearLambdaOutputFormatting = true; +}); +``` + +### InitTimeout + +Controls how long OnInit handlers can run before cancellation: + +```csharp +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.InitTimeout = TimeSpan.FromSeconds(10); +}); + +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + // 'ct' fires after 10 seconds + await cache.WarmUpAsync(ct); + return true; +}); +``` + +### InvocationCancellationBuffer + +Controls when the invocation cancellation token fires relative to Lambda timeout: + +```csharp +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Fire cancellation 5 seconds before Lambda times out + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); +}); + +lambda.MapHandler(async ([Event] Request request, CancellationToken ct) => +{ + // If Lambda timeout is 30s, 'ct' fires after 25s + await LongRunningOperationAsync(ct); + return new Response("Success"); +}); +``` + +### ShutdownDuration and ShutdownDurationBuffer + +Control shutdown timing: + +```csharp +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Time between SIGTERM and SIGKILL + options.ShutdownDuration = TimeSpan.FromSeconds(1); + + // Buffer to ensure completion + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); + + // Actual shutdown timeout: 1000ms - 100ms = 900ms +}); + +lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => +{ + // 'ct' fires after 900ms + await metrics.FlushAsync(ct); +}); +``` + +## Best Practices + +### ✅ Do: Keep OnInit Fast + +```csharp +// GOOD: Concurrent initialization +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + await cache.WarmUpAsync(ct); + return true; +}); + +lambda.OnInit(async (IDatabase db, CancellationToken ct) => +{ + await db.InitializeAsync(ct); + return true; +}); + +// Both run concurrently - faster cold start +``` + +### ❌ Don't: Perform Slow Sequential Operations + +```csharp +// BAD: Sequential initialization +lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +{ + var cache = services.GetRequiredService(); + await cache.WarmUpAsync(ct); // Wait... + + var db = services.GetRequiredService(); + await db.InitializeAsync(ct); // Then wait again... + + return true; +}); +``` + +### ✅ Do: Use CancellationToken in OnInit + +```csharp +// GOOD: Respects timeout +lambda.OnInit(async (IConfigService config, CancellationToken ct) => +{ + try + { + await config.LoadAsync(ct); + return true; + } + catch (OperationCanceledException) + { + Console.Error.WriteLine("Initialization timed out"); + return false; + } +}); +``` + +### ✅ Do: Flush Telemetry in OnShutdown + +```csharp +// GOOD: Ensure metrics are sent +lambda.OnShutdown(async (ITelemetry telemetry, CancellationToken ct) => +{ + await telemetry.FlushAsync(ct); +}); +``` + +### ❌ Don't: Ignore OnShutdown Errors + +```csharp +// BAD: Silent failure +lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => +{ + await metrics.FlushAsync(ct); // What if this fails? +}); +``` + +**Better:** + +```csharp +// GOOD: Log errors +lambda.OnShutdown(async (IMetrics metrics, ILogger logger, CancellationToken ct) => +{ + try + { + await metrics.FlushAsync(ct); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to flush metrics"); + } +}); +``` + +### ✅ Do: Return false on Critical OnInit Failures + +```csharp +// GOOD: Abort initialization on critical failure +lambda.OnInit(async (IDatabase db, ILogger logger, CancellationToken ct) => +{ + try + { + await db.ConnectAsync(ct); + return true; + } + catch (Exception ex) + { + logger.LogCritical(ex, "Failed to connect to database"); + return false; // Prevent invocations + } +}); +``` + +### ✅ Do: Use Singleton Services for OnInit/OnShutdown + +```csharp +// GOOD: Singleton persists across invocations +builder.Services.AddSingleton(); + +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + await cache.WarmUpAsync(ct); + return true; +}); + +lambda.OnShutdown(async (ICache cache, CancellationToken ct) => +{ + await cache.FlushAsync(ct); +}); + +// Same instance used in OnInit, invocations, and OnShutdown +``` + +## Anti-Patterns to Avoid + +### ❌ Blocking Async Code + +```csharp +// BAD: Blocking async operations +lambda.OnInit((ICache cache, CancellationToken ct) => +{ + cache.WarmUpAsync(ct).Wait(); // DON'T! + return Task.FromResult(true); +}); +``` + +**Better:** + +```csharp +// GOOD: Proper async/await +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + await cache.WarmUpAsync(ct); + return true; +}); +``` + +--- + +### ❌ Ignoring CancellationToken + +```csharp +// BAD: Ignoring timeout signal +lambda.OnInit(async (IConfigService config, CancellationToken ct) => +{ + await Task.Delay(TimeSpan.FromMinutes(5)); // Ignores 'ct'! + return true; +}); +``` + +**Better:** + +```csharp +// GOOD: Respect cancellation +lambda.OnInit(async (IConfigService config, CancellationToken ct) => +{ + await Task.Delay(TimeSpan.FromSeconds(3), ct); + return true; +}); +``` + +--- + +### ❌ Heavy Work in OnShutdown + +```csharp +// BAD: Too much work during shutdown +lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +{ + var processor = services.GetRequiredService(); + + // Processing 10,000 records during shutdown? No! + await processor.ProcessAllPendingRecordsAsync(ct); +}); +``` + +**Better:** + +```csharp +// GOOD: Minimal cleanup only +lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => +{ + // Just flush metrics + await metrics.FlushAsync(ct); +}); +``` + +--- + +### ❌ Returning false on Non-Critical Failures + +```csharp +// BAD: Aborting initialization for minor issues +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + try + { + await cache.WarmUpAsync(ct); + return true; + } + catch + { + // Cache warming failed, but Lambda can still work! + return false; // Don't abort for this + } +}); +``` + +**Better:** + +```csharp +// GOOD: Continue even if cache warming fails +lambda.OnInit(async (ICache cache, ILogger logger, CancellationToken ct) => +{ + try + { + await cache.WarmUpAsync(ct); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Cache warm-up failed, continuing anyway"); + } + + return true; // Continue initialization +}); +``` + +## Testing Lifecycle Handlers + +### Testing OnInit + +```csharp title="Tests/OnInitTests.cs" +using Xunit; +using NSubstitute; +using Microsoft.Extensions.DependencyInjection; + +public class OnInitTests +{ + [Fact] + public async Task OnInit_WarmsUpCache_ReturnsTrue() + { + // Arrange + var cache = Substitute.For(); + var services = new ServiceCollection() + .AddSingleton(cache) + .BuildServiceProvider(); + + var cts = new CancellationTokenSource(); + + // Act + var result = await OnInitHandler(services, cts.Token); + + // Assert + Assert.True(result); + await cache.Received(1).WarmUpAsync(cts.Token); + } + + private async Task OnInitHandler(IServiceProvider services, CancellationToken ct) + { + var cache = services.GetRequiredService(); + await cache.WarmUpAsync(ct); + return true; + } +} +``` + +### Testing OnShutdown + +```csharp title="Tests/OnShutdownTests.cs" +using Xunit; +using NSubstitute; +using Microsoft.Extensions.DependencyInjection; + +public class OnShutdownTests +{ + [Fact] + public async Task OnShutdown_FlushesMetrics() + { + // Arrange + var metrics = Substitute.For(); + var services = new ServiceCollection() + .AddSingleton(metrics) + .BuildServiceProvider(); + + var cts = new CancellationTokenSource(); + + // Act + await OnShutdownHandler(services, cts.Token); + + // Assert + await metrics.Received(1).FlushAsync(cts.Token); + } + + private async Task OnShutdownHandler(IServiceProvider services, CancellationToken ct) + { + var metrics = services.GetRequiredService(); + await metrics.FlushAsync(ct); + } +} +``` + +## Key Takeaways + +1. **OnInit** – Runs once on cold start for resource initialization +2. **OnShutdown** – Runs once before termination for cleanup +3. **Multiple Handlers** – OnInit runs concurrently, OnShutdown runs sequentially +4. **Return Values** – OnInit returns `true`/`false`; OnShutdown has no return value +5. **CancellationToken** – Always respect timeout signals +6. **Configuration** – Use `LambdaHostOptions` to configure timeouts +7. **DI Support** – Both phases support dependency injection +8. **Keep It Fast** – Minimize cold start time by keeping OnInit lean + +## Next Steps + +Now that you understand lifecycle management, explore related topics: + +- **[Dependency Injection](/guides/dependency-injection.md)** – Inject services into lifecycle handlers +- **[Configuration](/guides/configuration.md)** – Configure lifecycle timeouts +- **[Error Handling](/guides/error-handling.md)** – Handle errors in lifecycle handlers +- **[Testing](/guides/testing.md)** – Test lifecycle handlers in isolation +- **[Handler Registration](/guides/handler-registration.md)** – Understand the invocation phase + +--- + +Congratulations! You now understand how to control Lambda lifecycle phases for optimal performance and resource management. diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md new file mode 100644 index 00000000..46b2793f --- /dev/null +++ b/docs/guides/middleware.md @@ -0,0 +1,914 @@ +# Middleware + +Middleware provides a powerful mechanism for building composable pipelines around your Lambda handlers. Inspired by ASP.NET Core middleware, aws-lambda-host middleware enables cross-cutting concerns like logging, validation, metrics, error handling, and more—all without cluttering your handler code. + +## Introduction + +Middleware components execute in sequence, forming a pipeline around your handler: + +``` +Request → Middleware 1 → Middleware 2 → Handler → Middleware 2 → Middleware 1 → Response +``` + +Each middleware can: + +- Inspect the request before the handler executes +- Short-circuit the pipeline (skip the handler) +- Execute logic after the handler completes +- Handle exceptions from downstream components + +## Basic Middleware + +### Inline Middleware + +The simplest form of middleware is an inline lambda function: + +```csharp title="Program.cs" +using AwsLambda.Host; + +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine("Before handler"); + await next(context); + Console.WriteLine("After handler"); +}); + +lambda.MapHandler(([Event] Request request) => +{ + Console.WriteLine("Handler executing"); + return new Response("Success"); +}); + +await lambda.RunAsync(); +``` + +**Output:** +``` +Before handler +Handler executing +After handler +``` + +### Middleware Pipeline Composition + +Multiple middleware components execute in the order they're registered: + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine("[Middleware 1]: Before"); + await next(context); + Console.WriteLine("[Middleware 1]: After"); +}); + +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine("[Middleware 2]: Before"); + await next(context); + Console.WriteLine("[Middleware 2]: After"); +}); + +lambda.MapHandler(([Event] Request request) => +{ + Console.WriteLine("[Handler]: Executing"); + return new Response("Success"); +}); +``` + +**Output:** +``` +[Middleware 1]: Before +[Middleware 2]: Before +[Handler]: Executing +[Middleware 2]: After +[Middleware 1]: After +``` + +## ILambdaHostContext + +Middleware receives an `ILambdaHostContext` which provides access to invocation details and services. + +### Context Properties + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + // Access DI container + var logger = context.ServiceProvider.GetRequiredService>(); + + // Access cancellation token + if (context.CancellationToken.IsCancellationRequested) + { + logger.LogWarning("Request cancelled before handler"); + return; + } + + // Store invocation-scoped data + context.Items["RequestId"] = Guid.NewGuid().ToString(); + context.Items["StartTime"] = DateTimeOffset.UtcNow; + + await next(context); + + // Access stored data + var startTime = (DateTimeOffset)context.Items["StartTime"]; + var duration = DateTimeOffset.UtcNow - startTime; + logger.LogInformation("Request completed in {DurationMs}ms", duration.TotalMilliseconds); +}); +``` + +### Accessing Event and Response + +Use the `Features` collection to access typed event and response data: + +```csharp title="Program.cs" +using AwsLambda.Host.Abstractions.Features; + +lambda.UseMiddleware(async (context, next) => +{ + // Access the deserialized event + var eventFeature = context.Features.Get>(); + if (eventFeature != null) + { + var request = eventFeature.Event; + Console.WriteLine($"Processing request: {request.Name}"); + } + + await next(context); + + // Access the response + var responseFeature = context.Features.Get>(); + if (responseFeature != null) + { + var response = responseFeature.Response; + Console.WriteLine($"Response: {response.Message}"); + } +}); +``` + +## Common Middleware Patterns + +### Logging Middleware + +```csharp title="Program.cs" +using System.Diagnostics; + +lambda.UseMiddleware(async (context, next) => +{ + var logger = context.ServiceProvider.GetRequiredService>(); + var stopwatch = Stopwatch.StartNew(); + + logger.LogInformation("Request starting"); + + try + { + await next(context); + stopwatch.Stop(); + logger.LogInformation("Request completed successfully in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); + } + catch (Exception ex) + { + stopwatch.Stop(); + logger.LogError(ex, "Request failed after {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); + throw; + } +}); +``` + +### Error Handling Middleware + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch (ValidationException ex) + { + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogWarning(ex, "Validation failed: {Message}", ex.Message); + + // Set error response + var responseFeature = context.Features.Get>(); + if (responseFeature != null) + { + responseFeature.Response = new Response($"Validation error: {ex.Message}"); + } + + // Don't re-throw - error handled gracefully + } + catch (Exception ex) + { + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "Unhandled exception"); + throw; // Re-throw unhandled exceptions + } +}); +``` + +### Validation Middleware + +```csharp title="Program.cs" +using AwsLambda.Host.Abstractions.Features; + +lambda.UseMiddleware(async (context, next) => +{ + var eventFeature = context.Features.Get>(); + if (eventFeature != null) + { + var request = eventFeature.Event; + + // Validate request + if (string.IsNullOrEmpty(request.OrderId)) + { + throw new ValidationException("OrderId is required"); + } + + if (request.Amount <= 0) + { + throw new ValidationException("Amount must be positive"); + } + } + + await next(context); +}); +``` + +### Metrics Middleware + +```csharp title="Program.cs" +using System.Diagnostics; + +lambda.UseMiddleware(async (context, next) => +{ + var metrics = context.ServiceProvider.GetRequiredService(); + var stopwatch = Stopwatch.StartNew(); + + metrics.IncrementCounter("requests.total"); + + try + { + await next(context); + + stopwatch.Stop(); + metrics.RecordDuration("requests.duration", stopwatch.Elapsed); + metrics.IncrementCounter("requests.success"); + } + catch (Exception) + { + stopwatch.Stop(); + metrics.IncrementCounter("requests.failed"); + throw; + } +}); +``` + +### Caching Middleware + +```csharp title="Program.cs" +using AwsLambda.Host.Abstractions.Features; + +lambda.UseMiddleware(async (context, next) => +{ + var cache = context.ServiceProvider.GetRequiredService(); + var eventFeature = context.Features.Get>(); + + if (eventFeature != null) + { + var request = eventFeature.Event; + var cacheKey = $"request:{request.Id}"; + + // Check cache + if (cache.TryGet(cacheKey, out var cachedResponse)) + { + var responseFeature = context.Features.Get>(); + if (responseFeature != null) + { + responseFeature.Response = cachedResponse; + } + return; // Short-circuit - skip handler + } + + // Execute handler + await next(context); + + // Cache response + var finalResponseFeature = context.Features.Get>(); + if (finalResponseFeature?.Response != null) + { + cache.Set(cacheKey, finalResponseFeature.Response, TimeSpan.FromMinutes(5)); + } + } + else + { + await next(context); + } +}); +``` + +## Class-Based Middleware + +For reusable middleware, create a class with constructor injection. + +### Defining Middleware Classes + +```csharp title="Middleware/LoggingMiddleware.cs" +using AwsLambda.Host.Abstractions; +using Microsoft.Extensions.Logging; + +public class LoggingMiddleware +{ + private readonly ILogger _logger; + + public LoggingMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) + { + _logger.LogInformation("Request starting"); + + try + { + await next(context); + _logger.LogInformation("Request completed successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Request failed"); + throw; + } + } +} +``` + +### Registering Class-Based Middleware + +```csharp title="Program.cs" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +// Register middleware as Singleton +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +// Use class-based middleware +lambda.UseMiddleware(); + +lambda.MapHandler(([Event] Request request) => new Response("Success")); + +await lambda.RunAsync(); +``` + +### Multiple Class-Based Middleware + +```csharp title="Program.cs" +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +// Execution order: Logging → Metrics → Validation → Handler +lambda.UseMiddleware(); +lambda.UseMiddleware(); +lambda.UseMiddleware(); + +lambda.MapHandler(([Event] Request request) => new Response("Success")); +``` + +## Advanced Patterns + +### Conditional Middleware + +Execute middleware only under certain conditions: + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + var eventFeature = context.Features.Get>(); + + // Only log for specific request types + if (eventFeature?.Event?.Type == "order") + { + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogInformation("Processing order request"); + } + + await next(context); +}); +``` + +### Short-Circuiting + +Skip the handler and downstream middleware: + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + var cache = context.ServiceProvider.GetRequiredService(); + var eventFeature = context.Features.Get>(); + + if (eventFeature != null && cache.TryGet(eventFeature.Event.Id, out Response cached)) + { + var responseFeature = context.Features.Get>(); + if (responseFeature != null) + { + responseFeature.Response = cached; + } + return; // SHORT-CIRCUIT: Skip handler + } + + await next(context); // Continue pipeline +}); +``` + +### Middleware with Configuration + +```csharp title="Middleware/CachingMiddleware.cs" +using AwsLambda.Host.Abstractions; +using Microsoft.Extensions.Options; + +public class CachingMiddleware +{ + private readonly ICache _cache; + private readonly CachingOptions _options; + + public CachingMiddleware(ICache cache, IOptions options) + { + _cache = cache; + _options = options.Value; + } + + public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) + { + if (!_options.Enabled) + { + await next(context); + return; + } + + var eventFeature = context.Features.Get>(); + if (eventFeature != null) + { + var cacheKey = $"request:{eventFeature.Event.Id}"; + + if (_cache.TryGet(cacheKey, out var cached)) + { + var responseFeature = context.Features.Get>(); + if (responseFeature != null) + { + responseFeature.Response = cached; + } + return; + } + + await next(context); + + var finalResponseFeature = context.Features.Get>(); + if (finalResponseFeature?.Response != null) + { + _cache.Set(cacheKey, finalResponseFeature.Response, _options.CacheDuration); + } + } + else + { + await next(context); + } + } +} +``` + +```csharp title="Program.cs" +builder.Services.Configure( + builder.Configuration.GetSection("Caching") +); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var lambda = builder.Build(); +lambda.UseMiddleware(); +``` + +### Middleware Factory Pattern + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + var factory = context.ServiceProvider.GetRequiredService(); + var customMiddleware = factory.Create("validation"); + + await customMiddleware.InvokeAsync(context, next); +}); +``` + +## Execution Order + +Understanding middleware execution order is critical for building correct pipelines. + +### Registration Order + +Middleware executes in the order registered: + +```csharp title="Program.cs" +// Execution order: 1 → 2 → 3 → Handler → 3 → 2 → 1 +lambda.UseMiddleware(); // 1 +lambda.UseMiddleware(); // 2 +lambda.UseMiddleware(); // 3 +lambda.MapHandler(/* handler */); +``` + +### Typical Ordering + +```csharp title="Program.cs" +// 1. Error handling (catch all exceptions) +lambda.UseMiddleware(); + +// 2. Logging (log all requests) +lambda.UseMiddleware(); + +// 3. Metrics (measure all requests) +lambda.UseMiddleware(); + +// 4. Authentication (verify identity) +lambda.UseMiddleware(); + +// 5. Authorization (check permissions) +lambda.UseMiddleware(); + +// 6. Validation (validate input) +lambda.UseMiddleware(); + +// 7. Caching (cache responses) +lambda.UseMiddleware(); + +// 8. Handler +lambda.MapHandler(/* handler */); +``` + +**Why this order?** + +- **Error handling first** – Catches exceptions from all downstream middleware +- **Logging early** – Logs all requests (including failures) +- **Metrics early** – Measures all requests (including failures) +- **Auth before validation** – No point validating unauthenticated requests +- **Caching last** – Only cache authenticated, authorized, validated requests + +## State Management + +### Context.Items (Per-Invocation) + +Store temporary data scoped to a single invocation: + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + // Set invocation-scoped data + context.Items["RequestId"] = Guid.NewGuid().ToString(); + context.Items["UserId"] = "user123"; + + await next(context); +}); + +lambda.UseMiddleware(async (context, next) => +{ + // Access invocation-scoped data from previous middleware + var requestId = context.Items["RequestId"] as string; + var userId = context.Items["UserId"] as string; + + Console.WriteLine($"Request {requestId} for user {userId}"); + + await next(context); +}); +``` + +**Cleared after each invocation.** + +### Context.Properties (Cross-Invocation) + +Store shared data configured during the build phase: + +```csharp title="Program.cs" +var lambda = builder.Build(); + +// Set cross-invocation properties +lambda.Properties["Version"] = "1.0.0"; +lambda.Properties["Environment"] = "Production"; + +lambda.UseMiddleware(async (context, next) => +{ + // Access shared properties + var version = context.Properties["Version"] as string; + var env = context.Properties["Environment"] as string; + + Console.WriteLine($"App version {version} running in {env}"); + + await next(context); +}); +``` + +**Persists across invocations.** + +## Best Practices + +### ✅ Do: Keep Middleware Focused + +```csharp +// GOOD: Single responsibility +public class LoggingMiddleware +{ + public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) + { + _logger.LogInformation("Request starting"); + await next(context); + _logger.LogInformation("Request completed"); + } +} +``` + +### ❌ Don't: Mix Concerns in Middleware + +```csharp +// BAD: Too many responsibilities +public class EverythingMiddleware +{ + public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) + { + // Logging + _logger.LogInformation("Request starting"); + + // Metrics + _metrics.IncrementCounter("requests"); + + // Validation + if (string.IsNullOrEmpty(request.Id)) throw new ValidationException(); + + // Caching + if (_cache.TryGet(key, out var cached)) return cached; + + await next(context); + + // More logic... + } +} +``` + +### ✅ Do: Use Class-Based Middleware for Reusability + +```csharp +// GOOD: Reusable across projects +public class MetricsMiddleware +{ + private readonly IMetricsCollector _metrics; + + public MetricsMiddleware(IMetricsCollector metrics) + { + _metrics = metrics; + } + + public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) + { + _metrics.IncrementCounter("requests.total"); + await next(context); + } +} +``` + +### ✅ Do: Handle Exceptions Appropriately + +```csharp +// GOOD: Catch, log, and re-throw +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Request failed"); + throw; // Re-throw to let Lambda handle it + } +}); +``` + +### ❌ Don't: Swallow Exceptions Silently + +```csharp +// BAD: Silent failure +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch + { + // Exception swallowed - Lambda thinks it succeeded! + } +}); +``` + +### ✅ Do: Order Middleware Intentionally + +```csharp +// GOOD: Error handling wraps everything +lambda.UseMiddleware(); +lambda.UseMiddleware(); +lambda.UseMiddleware(); +``` + +### ❌ Don't: Place Error Handling Last + +```csharp +// BAD: Error handling can't catch validation errors +lambda.UseMiddleware(); +lambda.UseMiddleware(); // Too late! +``` + +## Anti-Patterns to Avoid + +### ❌ Blocking Async Code + +```csharp +// BAD: Blocking async operations +lambda.UseMiddleware(async (context, next) => +{ + var result = SomeAsyncMethod().Result; // DON'T! + await next(context); +}); +``` + +**Better approach:** + +```csharp +// GOOD: Proper async/await +lambda.UseMiddleware(async (context, next) => +{ + var result = await SomeAsyncMethod(); + await next(context); +}); +``` + +--- + +### ❌ Forgetting to Call next() + +```csharp +// BAD: Handler never executes +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine("Before handler"); + // Forgot to call next(context) - handler never runs! +}); +``` + +**Better approach:** + +```csharp +// GOOD: Always call next() unless short-circuiting intentionally +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine("Before handler"); + await next(context); + Console.WriteLine("After handler"); +}); +``` + +--- + +### ❌ Mutating Context Incorrectly + +```csharp +// BAD: Replacing context breaks downstream middleware +lambda.UseMiddleware(async (context, next) => +{ + context = new LambdaHostContext(); // DON'T! + await next(context); +}); +``` + +**Better approach:** + +```csharp +// GOOD: Use Items or Properties to store data +lambda.UseMiddleware(async (context, next) => +{ + context.Items["CustomData"] = "value"; + await next(context); +}); +``` + +## Testing Middleware + +### Unit Testing Inline Middleware + +```csharp title="Tests/MiddlewareTests.cs" +using Xunit; +using NSubstitute; +using AwsLambda.Host.Abstractions; + +public class MiddlewareTests +{ + [Fact] + public async Task LoggingMiddleware_LogsBeforeAndAfter() + { + // Arrange + var context = Substitute.For(); + var logger = Substitute.For>(); + var serviceProvider = Substitute.For(); + + serviceProvider.GetService(typeof(ILogger)).Returns(logger); + context.ServiceProvider.Returns(serviceProvider); + + var nextCalled = false; + Task Next(ILambdaHostContext ctx) + { + nextCalled = true; + return Task.CompletedTask; + } + + // Act + await LoggingMiddleware(context, Next); + + // Assert + Assert.True(nextCalled); + logger.Received(1).LogInformation(Arg.Any()); + } + + private async Task LoggingMiddleware(ILambdaHostContext context, LambdaInvocationDelegate next) + { + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogInformation("Request starting"); + await next(context); + } +} +``` + +### Unit Testing Class-Based Middleware + +```csharp title="Tests/LoggingMiddlewareTests.cs" +using Xunit; +using NSubstitute; +using AwsLambda.Host.Abstractions; + +public class LoggingMiddlewareTests +{ + [Fact] + public async Task InvokeAsync_LogsRequestStartAndCompletion() + { + // Arrange + var logger = Substitute.For>(); + var middleware = new LoggingMiddleware(logger); + var context = Substitute.For(); + + var nextCalled = false; + Task Next(ILambdaHostContext ctx) + { + nextCalled = true; + return Task.CompletedTask; + } + + // Act + await middleware.InvokeAsync(context, Next); + + // Assert + Assert.True(nextCalled); + logger.Received(1).LogInformation("Request starting"); + logger.Received(1).LogInformation("Request completed successfully"); + } +} +``` + +## Key Takeaways + +1. **Middleware Pipelines** – Compose cross-cutting concerns around handlers +2. **Execution Order** – Middleware executes in registration order +3. **ILambdaHostContext** – Access services, items, and features +4. **Class-Based Middleware** – Reusable with constructor injection +5. **Short-Circuiting** – Skip handler by not calling `next()` +6. **Error Handling** – Place error-handling middleware first +7. **State Management** – Use `Items` (per-invocation) or `Properties` (shared) +8. **Testing** – Unit test middleware in isolation + +## Next Steps + +Now that you understand middleware, explore related topics: + +- **[Dependency Injection](/guides/dependency-injection.md)** – Inject services into middleware +- **[Lifecycle Management](/guides/lifecycle-management.md)** – OnInit and OnShutdown hooks +- **[Error Handling](/guides/error-handling.md)** – Build error-handling middleware +- **[Testing](/guides/testing.md)** – Test middleware components +- **[Handler Registration](/guides/handler-registration.md)** – Understand handler execution + +--- + +Congratulations! You now understand how to build composable middleware pipelines for your Lambda functions. From 6fab5a828883be5dd64b264de733123fda4bb02b Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 08:53:38 -0500 Subject: [PATCH 03/65] feat(docs): enhance navigation configuration in mkdocs.yml - Added new navigation tab options: sticky, expand, path, prune, and indexes. - Integrated Table of Contents (toc) support with navigation. - Improved navigation top configuration for better user experience. --- mkdocs.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index efd77ccd..cda7d7bf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,14 @@ theme: - content.tabs.link - navigation.footer - navigation.top + - navigation.tabs + - navigation.tabs.sticky + - navigation.expand + - navigation.path + - navigation.prune + - navigation.indexes + - toc.integrate + - navigation.top palette: # Palette toggle for light mode From b2b6326437e8769db7809447eebbac5893d97b80 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 09:38:59 -0500 Subject: [PATCH 04/65] feat(docs): add comprehensive guide for handler registration - Documented `MapHandler` method for type-safe handler registration with examples. - Explained the `[Event]` attribute for deserialization and type safety. - Provided examples for dependency injection, async handlers, and return types. - Covered best practices, troubleshooting, and source generation optimizations. - Included detailed patterns for CRUD, validation, and cancellation handling. --- docs/guides/handler-registration.md | 687 ++++++++++++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100644 docs/guides/handler-registration.md diff --git a/docs/guides/handler-registration.md b/docs/guides/handler-registration.md new file mode 100644 index 00000000..491d4bfc --- /dev/null +++ b/docs/guides/handler-registration.md @@ -0,0 +1,687 @@ +# Handler Registration + +Handler registration is the core of aws-lambda-host. The `MapHandler` method combined with the +`[Event]` attribute provides type-safe, reflection-free handler registration with automatic +dependency injection—all powered by compile-time source generation. + +## Introduction + +Unlike traditional AWS Lambda handlers that rely on reflection and method naming conventions, +aws-lambda-host uses source generators and interceptors to analyze your handler at compile time, +generating optimized code with zero runtime overhead. + +**Benefits:** + +- ✅ **Zero reflection** – All parameter resolution happens at compile time +- ✅ **Type-safe** – Compiler errors for missing services or incorrect signatures +- ✅ **AOT ready** – Full support for Native AOT compilation +- ✅ **Better trimming** – Only required dependencies are included +- ✅ **Faster execution** – No reflection means faster cold starts + +## The MapHandler Method + +### Basic Handler + +The simplest handler takes an event parameter marked with `[Event]` and returns a response: + +```csharp title="Program.cs" +using AwsLambda.Host; + +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +lambda.MapHandler(([Event] string input) => $"Hello, {input}!"); + +await lambda.RunAsync(); +``` + +**How it works:** + +1. Source generator analyzes the handler signature +2. Generates deserialization code for `string` input +3. Generates serialization code for `string` output +4. Creates invocation wrapper—all at compile time + +### Handler with Services + +Inject registered services alongside the event: + +```csharp title="Program.cs" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddScoped(); + +var lambda = builder.Build(); + +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.Process(order) +); + +await lambda.RunAsync(); +``` + +**Source generation resolves:** + +- `[Event] Order order` → Deserialized from Lambda event +- `IOrderService service` → Resolved from DI container + +### Async Handlers + +Use `async` handlers for I/O-bound operations: + +```csharp title="Program.cs" +lambda.MapHandler(async ([Event] Order order, IOrderRepository repo) => +{ + var result = await repo.SaveAsync(order); + return new OrderResponse(result.Id, true); +}); +``` + +**Always prefer `async` for:** + +- Database operations +- HTTP requests +- File I/O +- Any awaitable operation + +## The [Event] Attribute + +The `[Event]` attribute marks the parameter that receives the deserialized Lambda event payload. + +### Purpose + +- **Identifies the event parameter** for source generation +- **Triggers code generation** for deserialization +- **Enables type-safe** event handling + +### Rules + +✅ **Required on exactly one parameter** + +```csharp +// GOOD: One [Event] parameter +lambda.MapHandler(([Event] Order order, IOrderService service) => ...); +``` + +❌ **Cannot be omitted** + +```csharp +// BAD: Missing [Event] attribute +lambda.MapHandler((Order order, IOrderService service) => ...); +// Compiler error: No event parameter marked with [Event] +``` + +❌ **Cannot mark multiple parameters** + +```csharp +// BAD: Multiple [Event] attributes +lambda.MapHandler(([Event] Order order, [Event] string id) => ...); +// Compiler error: Only one parameter can be marked with [Event] +``` + +### Valid Event Types + +The `[Event]` parameter can be any serializable type: + +```csharp +// Primitive types +lambda.MapHandler(([Event] string input) => ...); +lambda.MapHandler(([Event] int number) => ...); + +// Complex types +lambda.MapHandler(([Event] Order order) => ...); +lambda.MapHandler(([Event] OrderRequest request) => ...); + +// Collections +lambda.MapHandler(([Event] List orders) => ...); +lambda.MapHandler(([Event] Dictionary data) => ...); + +// Envelopes (with envelope packages) +lambda.MapHandler(([Event] ApiGatewayRequestEnvelope request) => ...); +lambda.MapHandler(([Event] SqsEventEnvelope sqsEvent) => ...); +``` + +## Injectable Parameters + +Handlers can inject multiple types of parameters. + +### All Injectable Types + +```csharp title="Program.cs" +lambda.MapHandler(async ( + [Event] Order order, // Lambda event (required) + IOrderService orderService, // Registered service + ICache cache, // Another registered service + ILambdaHostContext context, // Framework context + CancellationToken cancellationToken // Timeout signal +) => +{ + // Access invocation metadata + var requestId = context.Items["RequestId"]; + + // Use cancellation token for timeout handling + var result = await orderService.ProcessAsync(order, cancellationToken); + + return new OrderResponse(result.Id, true); +}); +``` + +### Parameter Resolution + +| Parameter Type | Description | Resolved From | +|----------------------|----------------------|-----------------------------------------| +| `[Event] T` | Lambda event payload | Deserialized from event JSON | +| `IServiceType` | Registered service | DI container (scoped per invocation) | +| `ILambdaHostContext` | Invocation context | Framework (provided per invocation) | +| `CancellationToken` | Timeout signal | Framework (fires before Lambda timeout) | + +### Parameter Order + +Parameter order doesn't matter—except `[Event]` must be present: + +```csharp +// All valid - order doesn't matter +lambda.MapHandler(([Event] Order order, IOrderService service, CancellationToken ct) => ...); +lambda.MapHandler((IOrderService service, [Event] Order order, CancellationToken ct) => ...); +lambda.MapHandler((CancellationToken ct, [Event] Order order, IOrderService service) => ...); +``` + +### Multiple Service Injection + +Inject as many services as needed: + +```csharp title="Program.cs" +lambda.MapHandler(async ( + [Event] OrderRequest request, + IOrderService orderService, + IInventoryService inventoryService, + IPaymentService paymentService, + INotificationService notificationService, + ILogger logger, + CancellationToken ct +) => +{ + logger.LogInformation("Processing order {OrderId}", request.OrderId); + + var inventoryOk = await inventoryService.CheckAsync(request.Items, ct); + if (!inventoryOk) return new OrderResponse { Success = false, Reason = "Out of stock" }; + + var paymentOk = await paymentService.ChargeAsync(request.Payment, ct); + if (!paymentOk) return new OrderResponse { Success = false, Reason = "Payment failed" }; + + var order = await orderService.CreateAsync(request, ct); + await notificationService.NotifyAsync(order, ct); + + return new OrderResponse { Success = true, OrderId = order.Id }; +}); +``` + +**⚠️ Caution:** Too many dependencies may indicate the handler is doing too much. Consider +delegating to a facade service. + +## Return Types + +Handler return values are automatically serialized to JSON. + +### Serialized Responses + +```csharp title="Program.cs" +// Return simple types +lambda.MapHandler(([Event] string input) => input.ToUpper()); +// Returns: "HELLO" (serialized as JSON string) + +// Return complex types +lambda.MapHandler(([Event] Order order) => + new OrderResponse(order.Id, true) +); +// Returns: {"orderId":"123","success":true} + +// Return collections +lambda.MapHandler(([Event] SearchRequest request, ISearchService search) => + search.Find(request.Query) +); +// Returns: [{"id":"1","name":"Item 1"}, ...] +``` + +### Void Returns + +Handlers can return `void` or `Task` for operations with no response: + +```csharp title="Program.cs" +lambda.MapHandler(([Event] LogEntry entry, ILogger logger) => +{ + logger.LogInformation("Log entry: {Entry}", entry); + // No return value +}); + +// Async void +lambda.MapHandler(async ([Event] Order order, IOrderRepository repo) => +{ + await repo.SaveAsync(order); + // No return value +}); +``` + +**Response:** Empty JSON response or `null` + +### Task vs ValueTask + +Both `Task` and `ValueTask` are supported: + +```csharp +// Task +lambda.MapHandler(async ([Event] Order order, IOrderService service) => + await service.ProcessAsync(order) +); + +// ValueTask - for hot paths +lambda.MapHandler(async ([Event] Order order, IOrderService service) => +{ + ValueTask result = service.ProcessAsync(order); + return await result; +}); +``` + +**Prefer `Task`** for most cases. Use `ValueTask` only for hot paths where allocation matters. + +## Source Generation + +Source generators analyze your handler at compile time and generate optimized code. + +### How It Works + +```csharp title="Program.cs" +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.Process(order) +); +``` + +**Generated code** (simplified): + +```csharp +// Deserialization +var order = JsonSerializer.Deserialize(eventJson); + +// Service resolution +var service = context.ServiceProvider.GetRequiredService(); + +// Invocation +var response = service.Process(order); + +// Serialization +var responseJson = JsonSerializer.Serialize(response); +``` + +**All generated at compile time—zero runtime reflection.** + +### Compile-Time Benefits + +✅ **Compile-time errors** for missing services: + +```csharp +lambda.MapHandler(([Event] Order order, IMissingService service) => ...); +// Compiler error if IMissingService not registered +``` + +✅ **Compile-time errors** for incorrect signatures: + +```csharp +lambda.MapHandler((Order order) => ...); +// Compiler error: Missing [Event] attribute +``` + +✅ **Optimized code generation**: + +```csharp +// Source generator creates optimized path for your exact signature +// No reflection, no dynamic dispatch, no runtime overhead +``` + +### Interceptors + +The framework uses C# 12 interceptors to replace the `MapHandler` call site with generated code: + +```csharp +// Your code +lambda.MapHandler(([Event] Order order) => ...); + +// Intercepted and replaced with +lambda.MapHandlerInterceptor0(([Event] Order order) => ...); +// Where MapHandlerInterceptor0 is generated with optimized code +``` + +**Result:** Zero-cost abstraction—as if you wrote the optimized code by hand. + +## Handler Patterns + +### Simple CRUD Handler + +```csharp title="Program.cs" +lambda.MapHandler(async ( + [Event] CreateOrderRequest request, + IOrderRepository repo +) => +{ + var order = new Order(request.CustomerId, request.Items, request.Total); + await repo.CreateAsync(order); + return new CreateOrderResponse(order.Id); +}); +``` + +### Handler with Validation + +```csharp title="Program.cs" +lambda.MapHandler(([Event] Order order, IValidator validator) => +{ + var validationResult = validator.Validate(order); + if (!validationResult.IsValid) + { + throw new ValidationException(validationResult.Errors); + } + + return new OrderResponse(order.Id, true); +}); +``` + +### Handler with Context Access + +```csharp title="Program.cs" +lambda.MapHandler(([Event] Request request, ILambdaHostContext context) => +{ + // Store request metadata + context.Items["RequestId"] = request.Id; + context.Items["Timestamp"] = DateTime.UtcNow; + + // Access shared properties + var appVersion = context.Properties["Version"] as string; + + // Process request... + return new Response("Success"); +}); +``` + +### Handler with Cancellation Token + +```csharp title="Program.cs" +lambda.MapHandler(async ( + [Event] Order order, + IOrderService service, + CancellationToken ct +) => +{ + try + { + return await service.ProcessAsync(order, ct); + } + catch (OperationCanceledException) + { + // Lambda timeout approaching + return new OrderResponse { Success = false, Reason = "Timeout" }; + } +}); +``` + +### Handler with Envelope + +```csharp title="Program.cs" +using AwsLambda.Host.Envelopes.ApiGateway; + +lambda.MapHandler(([Event] ApiGatewayRequestEnvelope request, ILogger logger) => +{ + logger.LogInformation("Request from IP: {IP}", request.RequestContext.Identity.SourceIp); + + // Access payload + var order = request.Body; + + // Process... + return new ApiGatewayResponseEnvelope + { + StatusCode = 200, + Body = new OrderResponse(order.Id, true) + }; +}); +``` + +### Thin Handler Pattern + +**Best Practice:** Keep handlers thin and delegate to services. + +```csharp title="Program.cs" +// GOOD: Thin handler delegates to service +lambda.MapHandler(([Event] Order order, IOrderProcessor processor) => + processor.ProcessAsync(order) +); +``` + +```csharp title="Services/OrderProcessor.cs" +public class OrderProcessor : IOrderProcessor +{ + private readonly IOrderRepository _repository; + private readonly IInventoryService _inventory; + private readonly IPaymentService _payment; + private readonly ILogger _logger; + + public OrderProcessor( + IOrderRepository repository, + IInventoryService inventory, + IPaymentService payment, + ILogger logger) + { + _repository = repository; + _inventory = inventory; + _payment = payment; + _logger = logger; + } + + public async Task ProcessAsync(Order order) + { + _logger.LogInformation("Processing order {OrderId}", order.Id); + + // Complex business logic here + var inventoryOk = await _inventory.ReserveAsync(order.Items); + if (!inventoryOk) throw new InvalidOperationException("Insufficient inventory"); + + var paymentOk = await _payment.ChargeAsync(order.Payment); + if (!paymentOk) throw new InvalidOperationException("Payment failed"); + + await _repository.SaveAsync(order); + + return new OrderResponse(order.Id, true); + } +} +``` + +**Why?** + +- ✅ Testable (test `OrderProcessor` in isolation) +- ✅ Reusable (use `OrderProcessor` in multiple handlers) +- ✅ Maintainable (business logic separated from handler) + +## Best Practices + +### ✅ Do: Keep Handlers Thin + +```csharp +// GOOD: Handler delegates to service +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.ProcessAsync(order) +); +``` + +### ❌ Don't: Put Business Logic in Handlers + +```csharp +// BAD: Business logic in handler +lambda.MapHandler(async ([Event] Order order, IOrderRepository repo, IInventoryService inventory) => +{ + // 50+ lines of business logic + if (order.Items.Count == 0) throw new ValidationException(); + var inventoryOk = await inventory.CheckAsync(order.Items); + if (!inventoryOk) throw new InvalidOperationException(); + // More logic... + return new OrderResponse(order.Id, true); +}); +``` + +### ✅ Do: Use Async/Await for I/O + +```csharp +// GOOD: Async handler for I/O operations +lambda.MapHandler(async ([Event] Order order, IOrderRepository repo) => + await repo.SaveAsync(order) +); +``` + +### ❌ Don't: Block Async Operations + +```csharp +// BAD: Blocking async code +lambda.MapHandler(([Event] Order order, IOrderRepository repo) => + repo.SaveAsync(order).Result // DON'T! +); +``` + +### ✅ Do: Inject Services, Not Factories + +```csharp +// GOOD: Inject service directly +lambda.MapHandler(([Event] Order order, IOrderService service) => + service.ProcessAsync(order) +); +``` + +### ❌ Don't: Use Service Locator Pattern + +```csharp +// BAD: Service locator anti-pattern +lambda.MapHandler(([Event] Order order, IServiceProvider services) => +{ + var service = services.GetRequiredService(); + return service.ProcessAsync(order); +}); +``` + +### ✅ Do: Return Strongly-Typed Responses + +```csharp +// GOOD: Strongly-typed response +lambda.MapHandler(([Event] Order order) => + new OrderResponse(order.Id, true) +); +``` + +### ❌ Don't: Return Anonymous Types + +```csharp +// BAD: Anonymous type (harder to test and maintain) +lambda.MapHandler(([Event] Order order) => + new { orderId = order.Id, success = true } +); +``` + +### ✅ Do: Use CancellationToken + +```csharp +// GOOD: Respect timeout signal +lambda.MapHandler(async ([Event] Order order, IOrderService service, CancellationToken ct) => + await service.ProcessAsync(order, ct) +); +``` + +## Troubleshooting + +### Handler Not Found Error + +**Error:** + +``` +System.InvalidOperationException: No handler registered +``` + +**Solution:** + +Ensure you call `MapHandler` before `RunAsync`: + +```csharp +lambda.MapHandler(([Event] string input) => ...); +await lambda.RunAsync(); // ✅ +``` + +### Missing [Event] Attribute + +**Error:** + +``` +Compiler error: No parameter marked with [Event] attribute +``` + +**Solution:** + +Mark the event parameter with `[Event]`: + +```csharp +lambda.MapHandler(([Event] Order order) => ...); // ✅ +``` + +### Service Not Registered + +**Error:** + +``` +System.InvalidOperationException: No service for type 'IOrderService' has been registered +``` + +**Solution:** + +Register the service before building: + +```csharp +builder.Services.AddScoped(); // ✅ +var lambda = builder.Build(); +``` + +### Multiple [Event] Attributes + +**Error:** + +``` +Compiler error: Only one parameter can be marked with [Event] +``` + +**Solution:** + +Only mark one parameter with `[Event]`: + +```csharp +lambda.MapHandler(([Event] Order order, IOrderService service) => ...); // ✅ +``` + +## Key Takeaways + +1. **MapHandler** – Registers your Lambda handler with type-safe DI +2. **[Event] Attribute** – Marks the event parameter (required on exactly one parameter) +3. **Injectable Parameters** – `[Event] T`, registered services, `ILambdaHostContext`, + `CancellationToken` +4. **Return Types** – Any serializable type or `void`/`Task` +5. **Source Generation** – Zero reflection, compile-time optimization, AOT ready +6. **Thin Handlers** – Delegate business logic to services +7. **Async/Await** – Always use async for I/O operations +8. **CancellationToken** – Handle Lambda timeouts gracefully + +## Next Steps + +Now that you understand handler registration, explore related topics: + +- **[Dependency Injection](/guides/dependency-injection.md)** – Inject services into handlers +- **[Middleware](/guides/middleware.md)** – Build middleware around handlers +- **[Error Handling](/guides/error-handling.md)** – Handle exceptions in handlers +- **[Testing](/guides/testing.md)** – Test handlers in isolation +- **[Configuration](/guides/configuration.md)** – Configure handler behavior + +--- + +Congratulations! You now understand how to register type-safe Lambda handlers with automatic +dependency injection. From 34a16dc9807daf36a7c8d7db7bc650374f492c0a Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 09:46:37 -0500 Subject: [PATCH 05/65] feat(docs): add configuration guide for Lambda functions - Introduced a comprehensive guide on framework and application configuration. - Documented `LambdaHostOptions` for control over lifecycle, timeouts, and serialization. - Explained application configuration patterns with `appsettings.json` and environment variables. - Added examples of strongly-typed options, validation, and environment-specific settings. - Covered JSON serialization, secrets management, and best practices for `IOptions`. - Included troubleshooting tips for common configuration issues. --- docs/guides/configuration.md | 790 +++++++++++++++++++++++++++++++++++ 1 file changed, 790 insertions(+) create mode 100644 docs/guides/configuration.md diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md new file mode 100644 index 00000000..a51cf07c --- /dev/null +++ b/docs/guides/configuration.md @@ -0,0 +1,790 @@ +# Configuration + +Proper configuration is essential for building robust Lambda functions. aws-lambda-host provides +flexible configuration through `LambdaHostOptions` for framework behavior, plus standard ASP.NET +Core configuration patterns for application settings. + +## Introduction + +Configuration in aws-lambda-host falls into two categories: + +1. **Framework Configuration** – `LambdaHostOptions` controls Lambda lifecycle, timeouts, and + serialization +2. **Application Configuration** – `appsettings.json`, environment variables, and the options + pattern for your business logic + +Both integrate seamlessly with `Microsoft.Extensions.Configuration` and dependency injection. + +## LambdaHostOptions + +`LambdaHostOptions` controls core framework behavior. + +### Configuration Method + +```csharp title="Program.cs" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.InitTimeout = TimeSpan.FromSeconds(10); + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); + options.ClearLambdaOutputFormatting = true; +}); + +var lambda = builder.Build(); +``` + +### Configuration Options Reference + +| Option | Type | Default | Description | +|--------------------------------|--------------------------|-----------|------------------------------------------------------------| +| `InitTimeout` | `TimeSpan` | 5 seconds | Maximum time for OnInit phase before cancellation | +| `InvocationCancellationBuffer` | `TimeSpan` | 3 seconds | Buffer before Lambda timeout to trigger cancellation token | +| `ShutdownDuration` | `TimeSpan` | 500ms | Time between SIGTERM and SIGKILL signals | +| `ShutdownDurationBuffer` | `TimeSpan` | 50ms | Safety buffer subtracted from ShutdownDuration | +| `ClearLambdaOutputFormatting` | `bool` | `false` | Remove Lambda runtime log formatting | +| `BootstrapHttpClient` | `HttpClient?` | `null` | Custom HTTP client for Lambda bootstrap | +| `BootstrapOptions` | `LambdaBootstrapOptions` | new() | Lambda runtime bootstrap configuration | + +### InitTimeout + +Controls how long `OnInit` handlers can run before cancellation. + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.InitTimeout = TimeSpan.FromSeconds(10); +}); + +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + // 'ct' fires after 10 seconds + await cache.WarmUpAsync(ct); + return true; +}); +``` + +**When to adjust:** + +- **Increase** if OnInit performs expensive operations (loading ML models, warming large caches) +- **Decrease** for faster feedback during initialization failures + +**Default:** 5 seconds + +### InvocationCancellationBuffer + +Controls when the invocation `CancellationToken` fires relative to Lambda's hard timeout. + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Fire cancellation 5 seconds before Lambda times out + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); +}); + +lambda.MapHandler(async ([Event] Order order, IOrderService service, CancellationToken ct) => +{ + // If Lambda timeout is 30s, 'ct' fires after 25s + return await service.ProcessAsync(order, ct); +}); +``` + +**Why it's needed:** + +- Allows graceful cancellation before Lambda's hard timeout +- Gives time for cleanup and error responses +- Prevents incomplete transactions + +**When to adjust:** + +- **Increase** for operations requiring significant cleanup time +- **Decrease** to maximize processing time per invocation + +**Default:** 3 seconds + +### ShutdownDuration + +Time between SIGTERM (shutdown signal) and SIGKILL (forced termination). + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Use predefined constant for external extensions + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500ms +}); +``` + +**Available Constants:** + +```csharp +// No extension time (0ms) +options.ShutdownDuration = ShutdownDuration.NoExtensions; + +// Internal extensions only (300ms) +options.ShutdownDuration = ShutdownDuration.InternalExtensions; + +// External extensions (500ms) - DEFAULT +options.ShutdownDuration = ShutdownDuration.ExternalExtensions; + +// Custom duration +options.ShutdownDuration = TimeSpan.FromSeconds(2); +``` + +**When to adjust:** + +- **Use NoExtensions** (0ms) if no Lambda extensions installed +- **Use InternalExtensions** (300ms) for internal-only extensions +- **Use ExternalExtensions** (500ms) if using external extensions (default) +- **Increase** if shutdown tasks require more time + +**Default:** `ShutdownDuration.ExternalExtensions` (500ms) + +### ShutdownDurationBuffer + +Safety buffer subtracted from `ShutdownDuration` to ensure cleanup completes before SIGKILL. + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.ShutdownDuration = TimeSpan.FromSeconds(1); + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); + + // Actual shutdown timeout: 1000ms - 100ms = 900ms +}); + +lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => +{ + // 'ct' fires after 900ms + await metrics.FlushAsync(ct); +}); +``` + +**Default:** 50ms + +### ClearLambdaOutputFormatting + +Removes Lambda runtime's custom log formatting, useful for structured logging frameworks. + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.ClearLambdaOutputFormatting = true; +}); +``` + +**Why use it:** + +- Lambda runtime adds formatting that interferes with JSON logging +- Structured logging (Serilog, NLog) produces malformed JSON without this +- CloudWatch Logs Insights parses logs correctly + +**Default:** `false` + +## Application Configuration + +Use standard ASP.NET Core configuration for application settings. + +### appsettings.json + +Create `appsettings.json` in your project root: + +```json title="appsettings.json" +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning" + } + }, + "OrderProcessing": { + "MaxRetries": 3, + "TimeoutSeconds": 30, + "EnableCaching": true + }, + "Database": { + "ConnectionString": "Server=localhost;Database=orders", + "CommandTimeout": 30 + }, + "ExternalApi": { + "BaseUrl": "https://api.example.com", + "ApiKey": "" + // Set via environment variable + } +} +``` + +**Important:** Include in `.csproj`: + +```xml + + + + PreserveNewest + + +``` + +### Options Pattern + +Define strongly-typed options classes: + +```csharp title="Configuration/OrderProcessingOptions.cs" +namespace MyLambda.Configuration; + +public class OrderProcessingOptions +{ + public int MaxRetries { get; init; } + public int TimeoutSeconds { get; init; } + public bool EnableCaching { get; init; } +} +``` + +Bind configuration sections to options: + +```csharp title="Program.cs" +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MyLambda.Configuration; + +var builder = LambdaApplication.CreateBuilder(); + +// Bind configuration section to options class +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing") +); + +builder.Services.AddScoped(); + +var lambda = builder.Build(); +``` + +Inject options into services: + +```csharp title="Services/OrderService.cs" +using Microsoft.Extensions.Options; +using MyLambda.Configuration; + +public class OrderService : IOrderService +{ + private readonly OrderProcessingOptions _options; + private readonly IOrderRepository _repository; + + public OrderService( + IOptions options, + IOrderRepository repository) + { + _options = options.Value; + _repository = repository; + } + + public async Task ProcessAsync(Order order) + { + if (_options.EnableCaching) + { + // Check cache + } + + for (int retry = 0; retry < _options.MaxRetries; retry++) + { + try + { + return await _repository.SaveAsync(order); + } + catch (Exception) when (retry < _options.MaxRetries - 1) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + + throw new Exception("Max retries exceeded"); + } +} +``` + +### IOptions vs IOptionsSnapshot vs IOptionsMonitor + +| Interface | Lifetime | Reloads | Use Case | +|-----------------------|-----------|----------------|---------------------------------------------------------------------| +| `IOptions` | Singleton | Never | **Recommended for Lambda** – Config doesn't change during execution | +| `IOptionsSnapshot` | Scoped | Per invocation | Use if config can change between invocations | +| `IOptionsMonitor` | Singleton | On change | Rarely needed in Lambda | + +**Recommendation:** Use `IOptions` for Lambda functions. + +```csharp +// GOOD: IOptions for Lambda +public OrderService(IOptions options) +{ + _options = options.Value; +} +``` + +## Environment-Specific Configuration + +Use multiple configuration files for different environments. + +### File Structure + +``` +MyLambda/ +├── appsettings.json # Base configuration +├── appsettings.Development.json # Development overrides +└── appsettings.Production.json # Production overrides +``` + +### Configuration Loading + +```csharp title="Program.cs" +var builder = LambdaApplication.CreateBuilder(); + +// Configuration loaded automatically in this order: +// 1. appsettings.json (base) +// 2. appsettings.{Environment}.json (environment-specific) +// 3. Environment variables (highest priority) + +// Add additional configuration sources if needed +builder.Configuration.AddEnvironmentVariables(); +``` + +### Environment-Specific Settings + +```json title="appsettings.Development.json" +{ + "OrderProcessing": { + "EnableCaching": false + }, + "Database": { + "ConnectionString": "Server=localhost;Database=orders_dev" + } +} +``` + +```json title="appsettings.Production.json" +{ + "OrderProcessing": { + "EnableCaching": true + }, + "Database": { + "ConnectionString": "" + // Set via environment variable + } +} +``` + +## Environment Variables + +Environment variables override configuration files. + +### Setting Environment Variables + +In Lambda, set environment variables through: + +- AWS Console +- AWS SAM template +- AWS CDK +- Terraform + +```yaml title="template.yaml" linenums="1" +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Environment: + Variables: + DATABASE_CONNECTION_STRING: !Ref DatabaseConnectionString + API_KEY: !Ref ApiKey + ASPNETCORE_ENVIRONMENT: Production +``` + +### Accessing Environment Variables + +#### Direct Access + +```csharp title="Program.cs" +var apiKey = Environment.GetEnvironmentVariable("API_KEY"); +``` + +#### Through Configuration + +```csharp title="Program.cs" +var apiKey = builder.Configuration["API_KEY"]; +``` + +#### Binding to Options + +```csharp title="Program.cs" +public class ExternalApiOptions +{ + public string BaseUrl { get; init; } = ""; + public string ApiKey { get; init; } = ""; +} + +builder.Services.Configure(options => +{ + options.BaseUrl = builder.Configuration["ExternalApi:BaseUrl"] ?? ""; + options.ApiKey = builder.Configuration["API_KEY"] ?? ""; +}); +``` + +### Environment Variable Naming + +Configuration binding supports hierarchical keys with `:` or `__`: + +```bash +# Both work +ExternalApi:BaseUrl=https://api.example.com +ExternalApi__BaseUrl=https://api.example.com +``` + +**Prefer `__`** for environment variables (`:` not supported in all shells). + +## JSON Serialization Configuration + +Configure JSON serialization for Lambda events and responses. + +### Default Serialization + +The framework uses `System.Text.Json` with sensible defaults: + +```csharp +// Default configuration (no action needed) +// - Camel case property names +// - Case-insensitive deserialization +// - Ignores null values +``` + +### Custom JsonSerializerContext (AOT) + +For Native AOT compilation, define a JSON serializer context: + +```csharp title="Program.cs" +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(Request))] +[JsonSerializable(typeof(Response))] +[JsonSerializable(typeof(Order))] +public partial class MySerializerContext : JsonSerializerContext; + +var builder = LambdaApplication.CreateBuilder(); + +// Register custom serializer context +builder.Services.AddLambdaSerializerWithContext(); + +var lambda = builder.Build(); +``` + +**Benefits:** + +- Required for Native AOT +- Compile-time serialization metadata +- Better trimming +- Faster performance + +### Custom JsonSerializerOptions + +Customize JSON serialization options: + +```csharp title="Program.cs" +using System.Text.Json; + +builder.Services.Configure(options => +{ + options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + options.WriteIndented = false; + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +}); +``` + +## Secrets Management + +Never hardcode secrets in configuration files. + +### AWS Secrets Manager + +```csharp title="Program.cs" +using Amazon; +using Amazon.Extensions.Configuration.SystemsManager; + +var builder = LambdaApplication.CreateBuilder(); + +// Add AWS Secrets Manager as configuration source +builder.Configuration.AddSecretsManager( + region: RegionEndpoint.USEast1, + configurator: options => + { + options.SecretFilter = entry => entry.Name.StartsWith("MyLambda/"); + options.PollingInterval = TimeSpan.FromMinutes(5); + } +); +``` + +**Install NuGet package:** + +```bash +dotnet add package Amazon.Extensions.Configuration.SystemsManager +``` + +### Environment Variables for Secrets + +```csharp title="Program.cs" +public class DatabaseOptions +{ + public string ConnectionString { get; init; } = ""; +} + +builder.Services.Configure(options => +{ + // Set via Lambda environment variable + options.ConnectionString = builder.Configuration["DATABASE_CONNECTION_STRING"] ?? ""; +}); +``` + +**In template.yaml:** + +```yaml +Environment: + Variables: + DATABASE_CONNECTION_STRING: !Sub '{{resolve:secretsmanager:${DatabaseSecret}:SecretString:connectionString}}' +``` + +## Configuration Validation + +Validate configuration at startup to catch errors early. + +### Data Annotations + +```csharp title="Configuration/OrderProcessingOptions.cs" +using System.ComponentModel.DataAnnotations; + +public class OrderProcessingOptions +{ + [Range(1, 10)] + public int MaxRetries { get; init; } + + [Range(1, 300)] + public int TimeoutSeconds { get; init; } + + public bool EnableCaching { get; init; } +} +``` + +### Validation on Startup + +```csharp title="Program.cs" +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("OrderProcessing")) + .ValidateDataAnnotations() + .ValidateOnStart(); +``` + +**Validation fails on startup** if configuration is invalid. + +### Custom Validation + +```csharp title="Configuration/OrderProcessingOptions.cs" +public class OrderProcessingOptions : IValidatableObject +{ + public int MaxRetries { get; init; } + public int TimeoutSeconds { get; init; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (MaxRetries < 1) + { + yield return new ValidationResult( + "MaxRetries must be at least 1", + new[] { nameof(MaxRetries) } + ); + } + + if (TimeoutSeconds > 300) + { + yield return new ValidationResult( + "TimeoutSeconds cannot exceed 300", + new[] { nameof(TimeoutSeconds) } + ); + } + } +} +``` + +## Best Practices + +### ✅ Do: Use Strongly-Typed Options + +```csharp +// GOOD: Strongly-typed options +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing") +); +``` + +### ❌ Don't: Use Magic Strings + +```csharp +// BAD: Magic strings everywhere +var maxRetries = int.Parse(builder.Configuration["OrderProcessing:MaxRetries"]); +var timeout = int.Parse(builder.Configuration["OrderProcessing:TimeoutSeconds"]); +``` + +### ✅ Do: Validate Configuration at Startup + +```csharp +// GOOD: Validate on startup +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("OrderProcessing")) + .ValidateDataAnnotations() + .ValidateOnStart(); +``` + +### ✅ Do: Use Environment-Specific Files + +```csharp +// GOOD: Environment-specific configuration +// appsettings.json +// appsettings.Development.json +// appsettings.Production.json +``` + +### ❌ Don't: Hardcode Configuration Values + +```csharp +// BAD: Hardcoded values +public class OrderService +{ + private const int MaxRetries = 3; + private const string ApiUrl = "https://api.example.com"; +} +``` + +### ✅ Do: Use Environment Variables for Secrets + +```csharp +// GOOD: Secrets via environment variables +var apiKey = builder.Configuration["API_KEY"]; +``` + +### ❌ Don't: Commit Secrets to Source Control + +```json +// BAD: API key in appsettings.json +{ + "ExternalApi": { + "ApiKey": "sk_live_51234567890abcdef" + // DON'T! + } +} +``` + +### ✅ Do: Include appsettings.json in Deployment + +```xml + + + + PreserveNewest + + +``` + +### ✅ Do: Use IOptions for Lambda + +```csharp +// GOOD: IOptions for Lambda (config doesn't reload) +public OrderService(IOptions options) +{ + _options = options.Value; +} +``` + +### ❌ Don't: Use IOptionsMonitor in Lambda + +```csharp +// BAD: IOptionsMonitor rarely needed in Lambda +public OrderService(IOptionsMonitor options) +{ + _options = options.CurrentValue; // Unnecessary overhead +} +``` + +## Troubleshooting + +### Configuration Section Not Found + +**Problem:** `options.Value` is null or has default values. + +**Solution:** Verify configuration section exists and binding is correct: + +```csharp +// Check configuration exists +var section = builder.Configuration.GetSection("OrderProcessing"); +if (!section.Exists()) +{ + throw new InvalidOperationException("OrderProcessing configuration section not found"); +} + +// Bind with validation +builder.Services.AddOptions() + .Bind(section) + .ValidateDataAnnotations() + .ValidateOnStart(); +``` + +### appsettings.json Not Found + +**Problem:** Configuration file not deployed with Lambda. + +**Solution:** Ensure `appsettings.json` is copied to output: + +```xml + + + + PreserveNewest + + +``` + +### Environment Variable Not Loaded + +**Problem:** Environment variable set but not accessible. + +**Solution:** Ensure environment variables are loaded: + +```csharp +builder.Configuration.AddEnvironmentVariables(); +``` + +**Or:** Check Lambda environment variable configuration in AWS Console/template. + +## Key Takeaways + +1. **LambdaHostOptions** – Configure framework behavior (timeouts, shutdown, serialization) +2. **appsettings.json** – Store application configuration +3. **Options Pattern** – Use `IOptions` for strongly-typed configuration +4. **Environment Variables** – Override configuration, store secrets +5. **Validation** – Validate configuration at startup +6. **JSON Serialization** – Customize with `JsonSerializerOptions` or `JsonSerializerContext` +7. **Secrets** – Use environment variables or AWS Secrets Manager, never commit secrets +8. **IOptions** – Prefer over `IOptionsSnapshot` or `IOptionsMonitor` for Lambda + +## Next Steps + +Now that you understand configuration, explore related topics: + +- **[Lifecycle Management](/guides/lifecycle-management.md)** – Use configuration in OnInit and + OnShutdown +- **[Dependency Injection](/guides/dependency-injection.md)** – Inject configured options into + services +- **[Error Handling](/guides/error-handling.md)** – Configure timeout buffers +- **[Deployment](/guides/deployment.md)** – Set environment variables in deployment templates +- **[Testing](/guides/testing.md)** – Test services with different configurations + +--- + +Congratulations! You now understand how to configure aws-lambda-host and your Lambda functions. From a88e6236793f7e0911d96e2d5d6e24dc8d53aaa6 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 09:49:57 -0500 Subject: [PATCH 06/65] feat(docs): update mkdocs.yml with new styling and guides - Updated light and dark mode palettes with orange primary and accent colors. - Added a new "Guides" section with links to various framework documentation topics. - Removed unused `toc.integrate` option from navigation. - Improved structure for better theme customization and navigation clarity. --- AwsLambda.Host.sln | 1 + mkdocs.yml | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/AwsLambda.Host.sln b/AwsLambda.Host.sln index 2e9c52a8..4b52afef 100644 --- a/AwsLambda.Host.sln +++ b/AwsLambda.Host.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Packages.props = Directory.Packages.props tasks\FormattingTasks.yml = tasks\FormattingTasks.yml THIRD-PARTY-LICENSES.txt = THIRD-PARTY-LICENSES.txt + mkdocs.yml = mkdocs.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" diff --git a/mkdocs.yml b/mkdocs.yml index cda7d7bf..ab8c78ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,18 +24,22 @@ theme: - navigation.path - navigation.prune - navigation.indexes - - toc.integrate - navigation.top palette: # Palette toggle for light mode - scheme: default + primary: orange + accent: orange toggle: icon: material/brightness-7 name: Switch to dark mode + # Palette toggle for dark mode - scheme: slate + primary: orange + accent: orange toggle: icon: material/brightness-4 name: Switch to light mode @@ -77,6 +81,19 @@ nav: - Your First Lambda: getting-started/first-lambda.md - Core Concepts: getting-started/core-concepts.md - Project Structure: getting-started/project-structure.md + - Guides: + - guides/index.md + - Dependency Injection: guides/dependency-injection.md + - Middleware: guides/middleware.md + - Lifecycle Management: guides/lifecycle-management.md + - Handler Registration: guides/handler-registration.md + - Configuration: guides/configuration.md + - Error Handling: guides/error-handling.md + - Testing: guides/testing.md + - Deployment: guides/deployment.md plugins: - - search \ No newline at end of file + - search + +#extra_css: +# - stylesheets/extra.css \ No newline at end of file From 682c40b0256e00415df758252cd43f1d2f96b9d3 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 10:18:30 -0500 Subject: [PATCH 07/65] feat(docs): update primary color and include custom stylesheet - Changed primary color in dark mode to black in mkdocs.yml configuration. - Enabled usage of custom `extra.css` for additional styling. - Added orange theme color definitions for primary, light, and dark variants in `extra.css`. --- docs/stylesheets/extra.css | 12 ++++++++++++ mkdocs.yml | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 docs/stylesheets/extra.css diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..f91f08c9 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,12 @@ +[data-md-color-primary="orange"] { + --md-primary-fg-color: #F57C00; + --md-primary-fg-color--light: #FF9800; + --md-primary-fg-color--dark: #E65100; +} + +[data-md-color-scheme="slate"][data-md-color-primary="black"] { + --md-primary-fg-color: #F57C00; + --md-primary-fg-color--light: #FF9800; + --md-primary-fg-color--dark: #E65100; + --md-typeset-a-color: #F57C00; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index ab8c78ae..7a80b43d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,7 +38,7 @@ theme: # Palette toggle for dark mode - scheme: slate - primary: orange + primary: black accent: orange toggle: icon: material/brightness-4 @@ -95,5 +95,5 @@ nav: plugins: - search -#extra_css: -# - stylesheets/extra.css \ No newline at end of file +extra_css: + - stylesheets/extra.css \ No newline at end of file From bfc4e4adfa76b654669b26ef7a000714b4dcf76b Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 10:37:39 -0500 Subject: [PATCH 08/65] feat(docs): add comprehensive guide for error handling in Lambda - Introduced detailed documentation on error handling strategies in AWS Lambda functions. - Covered handler, middleware, and lifecycle error handling patterns with examples. - Documented cancellation token management, including `InvocationCancellationBuffer` configuration. - Explained retry strategies, application-level retries, and AWS-native retry behaviors. - Added best practices, anti-patterns, and troubleshooting guidance for error handling. - Included examples of structured error responses, exception transformation, and logging. - Detailed lifecycle error handling during OnInit and OnShutdown phases. --- docs/guides/error-handling.md | 965 ++++++++++++++++++++++++++++++++++ 1 file changed, 965 insertions(+) create mode 100644 docs/guides/error-handling.md diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md new file mode 100644 index 00000000..854ebdaf --- /dev/null +++ b/docs/guides/error-handling.md @@ -0,0 +1,965 @@ +# Error Handling + +Comprehensive error handling is essential for building resilient AWS Lambda functions. This guide +covers exception handling patterns, cancellation token usage, lifecycle error handling, retry +strategies, and AWS Lambda integration patterns specific to the aws-lambda-host framework. + +--- + +## Introduction + +AWS Lambda functions face unique error handling challenges: + +- **Timeout constraints**: Lambda functions must complete within a configured timeout +- **Cold starts**: Initialization failures prevent the function from starting +- **Graceful shutdown**: Limited time window for cleanup before container termination +- **Retry behavior**: Event source determines retry semantics + +The aws-lambda-host framework provides built-in error handling mechanisms while allowing you to +implement custom error handling strategies. + +--- + +## Handler Exception Handling + +### Exception Bubbling + +Exceptions thrown in Lambda handlers naturally bubble up through the middleware pipeline. This +allows middleware to intercept, log, or transform exceptions before they reach the Lambda runtime. + +```csharp title="Program.cs" +lambda.MapHandler(async ([Event] OrderRequest request, IOrderService service) => +{ + // Unhandled exceptions bubble through middleware to Lambda runtime + return await service.ProcessAsync(request); +}); +``` + +**How it works**: + +1. Handler throws exception +2. Exception propagates through middleware pipeline (innermost to outermost) +3. Each middleware can catch, log, or re-throw +4. If unhandled, Lambda runtime receives the exception + +### Try-Catch in Handlers + +For fine-grained error handling, use try-catch blocks to handle specific exception types and return +error responses. + +```csharp title="Program.cs" linenums="1" +lambda.MapHandler(async ([Event] OrderRequest request, IOrderService service) => +{ + try + { + var result = await service.ProcessAsync(request); + return new OrderResponse + { + OrderId = result.Id, + Status = "Success" + }; + } + catch (ValidationException ex) + { + // Return structured error response for validation failures + return new OrderResponse + { + Status = "ValidationError", + Error = ex.Message + }; + } + catch (InvalidOperationException ex) + { + // Log and return business logic errors + Console.WriteLine($"Business logic error: {ex.Message}"); + return new OrderResponse + { + Status = "BusinessError", + Error = "Unable to process order" + }; + } + // Let other exceptions bubble to middleware/runtime +}); +``` + +!!! tip "When to catch exceptions in handlers" + +- Validation errors that should return specific HTTP status codes or error structures +- Expected business logic exceptions (insufficient inventory, duplicate orders, etc.) +- Errors requiring custom response formatting + +--- + +## Middleware Error Handling + +Middleware is ideal for global error handling policies that apply to all invocations. + +### Global Error Handler Pattern + +```csharp title="Program.cs" linenums="1" +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch (ValidationException ex) + { + // Log validation errors with context + Console.WriteLine($"[Validation Error] RequestId: {context.LambdaContext.AwsRequestId}, Error: {ex.Message}"); + + // Set error response (if using API Gateway integration) + // context.Items["StatusCode"] = 400; + // context.Items["ErrorMessage"] = ex.Message; + + // Re-throw to Lambda runtime (which may have retry logic) + throw; + } + catch (OperationCanceledException) + { + // Log timeout/cancellation + Console.WriteLine($"[Timeout] RequestId: {context.LambdaContext.AwsRequestId} cancelled"); + throw; + } + catch (Exception ex) + { + // Log unexpected errors with full stack trace + Console.WriteLine($"[Unexpected Error] RequestId: {context.LambdaContext.AwsRequestId}"); + Console.WriteLine($"Exception: {ex}"); + + // Optional: Send to error tracking service (Sentry, Rollbar, etc.) + // await errorTracker.CaptureAsync(ex); + + throw; + } +}); +``` + +### Error Transformation Middleware + +Transform exceptions into structured error responses before they reach the Lambda runtime. + +```csharp title="Program.cs" linenums="1" +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch (ValidationException ex) + { + // Transform validation exception into error response + var errorResponse = new ErrorResponse + { + Type = "ValidationError", + Message = ex.Message, + RequestId = context.LambdaContext.AwsRequestId, + Timestamp = DateTime.UtcNow + }; + + // Serialize error response to context + var responseFeature = context.Features.Get(); + if (responseFeature != null) + { + // Set error response (implementation depends on response handling) + context.Items["ErrorResponse"] = errorResponse; + } + + // Don't re-throw - error has been handled + } + catch (Exception ex) + { + // Log and re-throw unhandled errors + Console.WriteLine($"Unhandled error: {ex}"); + throw; + } +}); +``` + +!!! warning "Middleware execution order matters" +Error handling middleware should typically be registered first (outer layer) so it can catch +exceptions from all subsequent middleware and handlers. + +--- + +## Cancellation Token Handling + +AWS Lambda enforces strict timeout limits. The framework provides automatic cancellation token +management to ensure your code can gracefully handle timeouts. + +### InvocationCancellationBuffer + +The `InvocationCancellationBuffer` configuration prevents hard Lambda timeouts by firing the +cancellation token before Lambda terminates your function. + +**How it works**: + +``` +Lambda Timeout = 30 seconds +InvocationCancellationBuffer = 3 seconds (default) +------------------------ +CancellationToken fires at: 27 seconds +Lambda hard timeout at: 30 seconds +``` + +This gives your code 3 seconds to complete current operations, flush metrics, and gracefully exit +before Lambda forcefully terminates the container. + +```csharp title="Program.cs" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Default is 3 seconds + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); +}); +``` + +### Timeout Calculation + +From `DefaultLambdaCancellationFactory`: + +```csharp +var maxAllowedDuration = context.RemainingTime - _bufferDuration; + +if (maxAllowedDuration <= TimeSpan.Zero) +{ + throw new InvalidOperationException( + "CancellationTokenSource provided with insufficient time. " + + $"Lambda Remaining Time = {context.RemainingTime:c}, " + + $"Cancellation Token Buffer = {_bufferDuration:c}, " + + $"Candidate Token Duration = {maxAllowedDuration:c}" + ); +} +``` + +!!! danger "Insufficient time error" +If `InvocationCancellationBuffer` exceeds the Lambda's remaining time, the framework throws +`InvalidOperationException` with diagnostic information. This typically occurs when: + + - Lambda timeout is very short (< 3 seconds) + - Buffer is configured larger than Lambda timeout + - Previous operations consumed too much time + +### Respecting Cancellation Tokens + +Always pass and respect cancellation tokens in async operations: + +```csharp title="Program.cs" linenums="1" +lambda.MapHandler(async ( + [Event] OrderRequest request, + IOrderService orderService, + IHttpClientFactory httpClientFactory, + CancellationToken cancellationToken) => +{ + // Pass cancellation token to all async operations + var order = await orderService.CreateOrderAsync(request, cancellationToken); + + // HTTP calls should respect cancellation + var httpClient = httpClientFactory.CreateClient(); + var response = await httpClient.PostAsJsonAsync( + "https://api.example.com/notify", + order, + cancellationToken + ); + + return new OrderResponse { OrderId = order.Id }; +}); +``` + +### Handling OperationCanceledException + +When a cancellation token fires, async operations throw `OperationCanceledException`: + +```csharp title="Program.cs" linenums="1" +lambda.MapHandler(async ( + [Event] ProcessingRequest request, + IDataService dataService, + CancellationToken cancellationToken) => +{ + try + { + var data = await dataService.ProcessLargeDatasetAsync(request.DatasetId, cancellationToken); + return new ProcessingResponse { Status = "Completed", RecordsProcessed = data.Count }; + } + catch (OperationCanceledException) + { + // Timeout occurred - return partial results or failure status + Console.WriteLine($"Processing timeout for dataset {request.DatasetId}"); + return new ProcessingResponse + { + Status = "Timeout", + RecordsProcessed = dataService.GetProcessedCount() + }; + } +}); +``` + +!!! tip "Best practice for long-running operations" + +- Check `cancellationToken.IsCancellationRequested` periodically in loops +- Pass cancellation tokens to all I/O operations (HTTP, database, file operations) +- Catch `OperationCanceledException` to handle graceful degradation +- Flush any buffered data before returning on cancellation + +--- + +## Lifecycle Error Handling + +The OnInit and OnShutdown lifecycle phases have specialized error handling behavior. + +### OnInit Error Handling + +**Behavior**: + +- All OnInit handlers execute **in parallel** +- Handlers have an `InitTimeout` (default 5 seconds) +- Each handler gets its own **service scope** +- Exceptions are caught and **aggregated** +- Handlers can return `false` to **abort startup without throwing** +- All handlers complete, then errors are bundled into `AggregateException` + +```csharp title="Program.cs" linenums="1" +lambda.OnInit(async (ICache cache, CancellationToken cancellationToken) => +{ + try + { + // Warm up cache during cold start + await cache.WarmUpAsync(cancellationToken); + return true; // Startup succeeds + } + catch (OperationCanceledException) + { + // InitTimeout fired (default 5 seconds) + Console.WriteLine("Cache warmup timeout"); + return false; // Abort startup - prevents Lambda from starting + } + catch (Exception ex) + { + // Log initialization error + Console.WriteLine($"Cache warmup failed: {ex.Message}"); + + // Option 1: Return false to abort startup gracefully + return false; + + // Option 2: Throw to include in AggregateException + // throw; + } +}); + +lambda.OnInit(async (IDatabaseMigrator migrator, CancellationToken cancellationToken) => +{ + // Second handler - runs in parallel with first + await migrator.EnsureMigratedAsync(cancellationToken); + return true; +}); +``` + +**Configuration**: + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Maximum time for all OnInit handlers (default 5 seconds) + options.InitTimeout = TimeSpan.FromSeconds(10); +}); +``` + +**Error scenarios**: + +| Scenario | Handler 1 | Handler 2 | Result | +|-------------------|--------------------|--------------------|-------------------------------------------| +| Both succeed | `return true` | `return true` | Lambda starts | +| One returns false | `return false` | `return true` | Lambda aborts (no exception) | +| One throws | throws `Exception` | `return true` | `AggregateException` thrown | +| Both throw | throws `Exception` | throws `Exception` | `AggregateException` with both exceptions | + +!!! note "Parallel execution" +From `LambdaOnInitBuilder.cs`: + +```csharp +var tasks = _handlers.Select(h => RunInitHandler(h, cts.Token)); +var results = await Task.WhenAll(tasks).ConfigureAwait(false); +``` + +All init handlers run concurrently, not sequentially. + +### OnShutdown Error Handling + +**Behavior**: + +- All OnShutdown handlers execute **in parallel** +- Each handler gets its own **service scope** +- Individual exceptions are caught and collected +- All handlers complete before throwing `AggregateException` +- **Does not interrupt execution of other handlers** + +```csharp title="Program.cs" linenums="1" +lambda.OnShutdown(async (IMetricsService metrics, CancellationToken cancellationToken) => +{ + try + { + // Flush metrics before shutdown + await metrics.FlushAsync(cancellationToken); + } + catch (Exception ex) + { + // Log shutdown error - won't prevent other handlers from running + Console.WriteLine($"Metrics flush error: {ex.Message}"); + + // Error is aggregated but other shutdown handlers continue + throw; + } +}); + +lambda.OnShutdown(async (IConnectionPool connectionPool, CancellationToken cancellationToken) => +{ + // Runs in parallel with metrics flush + await connectionPool.DrainAsync(cancellationToken); +}); +``` + +**Shutdown timing configuration**: + +```csharp title="Program.cs" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Time between SIGTERM and SIGKILL (default 500ms for external extensions) + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500ms + + // options.ShutdownDuration = ShutdownDuration.InternalExtensions; // 300ms + // options.ShutdownDuration = ShutdownDuration.NoExtensions; // 0ms + + // Buffer to ensure cleanup completes (default 50ms) + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); +}); +``` + +**Shutdown timeline**: + +``` +1. AWS sends SIGTERM +2. ShutdownDuration starts (500ms for ExternalExtensions) +3. CancellationToken fires at (ShutdownDuration - ShutdownDurationBuffer) = 450ms +4. OnShutdown handlers execute with 450ms to complete +5. AWS sends SIGKILL at 500ms (forceful termination) +``` + +!!! warning "Limited shutdown time" +Shutdown handlers must complete quickly (typically < 500ms). Long-running cleanup operations may be +terminated by SIGKILL. Use cancellation tokens to detect imminent shutdown. + +--- + +## Application-Level Retry Patterns + +The aws-lambda-host framework **does not provide built-in retry mechanisms**. You implement retry +logic in your application code or use libraries like Polly. + +### Simple Exponential Backoff + +```csharp title="RetryHelper.cs" linenums="1" +public static class RetryHelper +{ + public static async Task RetryAsync( + Func> operation, + int maxRetries = 3, + CancellationToken cancellationToken = default) + { + for (int i = 0; i < maxRetries; i++) + { + try + { + return await operation(); + } + catch (Exception ex) when (i < maxRetries - 1 && IsTransientError(ex)) + { + var delay = TimeSpan.FromSeconds(Math.Pow(2, i)); // 1s, 2s, 4s + Console.WriteLine($"Retry {i + 1}/{maxRetries} after {delay.TotalSeconds}s due to: {ex.Message}"); + await Task.Delay(delay, cancellationToken); + } + } + + throw new Exception($"Operation failed after {maxRetries} retries"); + } + + private static bool IsTransientError(Exception ex) + { + return ex is HttpRequestException or TimeoutException or IOException; + } +} +``` + +**Usage**: + +```csharp title="Program.cs" +lambda.MapHandler(async ([Event] Request request, IHttpClientFactory httpClientFactory, CancellationToken ct) => +{ + var httpClient = httpClientFactory.CreateClient(); + + var response = await RetryHelper.RetryAsync(async () => + await httpClient.GetAsync($"https://api.example.com/data/{request.Id}", ct), + maxRetries: 3, + cancellationToken: ct + ); + + return await response.Content.ReadAsStringAsync(ct); +}); +``` + +### Using Polly + +[Polly](https://github.com/App-vNext/Polly) is the recommended library for resilience and transient +fault handling. + +**Installation**: + +```bash +dotnet add package Polly +``` + +**Retry policy**: + +```csharp title="Program.cs" linenums="1" +using Polly; +using Polly.Retry; + +// Configure resilience in service registration +builder.Services.AddSingleton(sp => +{ + return Policy + .Handle() + .Or() + .WaitAndRetryAsync( + retryCount: 3, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + onRetry: (exception, timeSpan, retryCount, context) => + { + Console.WriteLine($"Retry {retryCount} after {timeSpan.TotalSeconds}s due to {exception.Message}"); + } + ); +}); + +// Use in handler +lambda.MapHandler(async ( + [Event] Request request, + IHttpClientFactory httpClientFactory, + AsyncRetryPolicy retryPolicy, + CancellationToken ct) => +{ + var httpClient = httpClientFactory.CreateClient(); + + var response = await retryPolicy.ExecuteAsync(async () => + await httpClient.GetAsync($"https://api.example.com/data/{request.Id}", ct) + ); + + return await response.Content.ReadAsStringAsync(ct); +}); +``` + +**Advanced Polly policies**: + +```csharp title="Program.cs" linenums="1" +using Polly; +using Polly.CircuitBreaker; + +// Circuit breaker policy +var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreakerAsync( + handledEventsAllowedBeforeBreaking: 3, + durationOfBreak: TimeSpan.FromSeconds(30) + ); + +// Timeout policy +var timeoutPolicy = Policy + .TimeoutAsync(TimeSpan.FromSeconds(10)); + +// Combine policies (timeout → retry → circuit breaker) +var combinedPolicy = Policy.WrapAsync(circuitBreakerPolicy, retryPolicy, timeoutPolicy); + +await combinedPolicy.ExecuteAsync(async () => + await httpClient.GetAsync(url, cancellationToken) +); +``` + +!!! tip "Retry best practices" + +- Only retry **transient errors** (network failures, timeouts, 5xx HTTP responses) +- **Don't retry** validation errors, authentication failures, or 4xx responses +- Use **exponential backoff** to avoid overwhelming downstream services +- Add **jitter** to prevent thundering herd problem +- Respect **Lambda timeout** - ensure retries complete before cancellation + +--- + +## AWS Lambda Integration + +### Event Source Retries + +AWS Lambda provides built-in retry mechanisms for asynchronous event sources: + +| Event Source | Retry Behavior | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------| +| **SQS** | Configurable `maxReceiveCount` on queue. After retries exhausted, message moves to DLQ (if configured). | +| **EventBridge** | Automatic retries for up to 24 hours with exponential backoff. | +| **Kinesis / DynamoDB Streams** | Retries until record expires or processed successfully. Use `BisectBatchOnFunctionError` to isolate poison pills. | +| **SNS** | Retries twice with exponential backoff. After failure, message discarded (configure DLQ on SNS topic). | +| **S3** | No automatic retries. Use S3 Event Notifications with SQS for retry capability. | + +**Synchronous invocations** (API Gateway, ALB, Lambda Function URLs): + +- **No automatic retries** - client must retry +- Errors return immediately to caller +- Use application-level retries (Polly) or API Gateway retry configuration + +### Dead Letter Queues (DLQ) + +DLQs are configured **at the AWS Lambda level**, not in the application code. + +**Asynchronous invocations**: + +```yaml title="template.yaml (SAM)" linenums="1" +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: bootstrap + Runtime: provided.al2023 + DeadLetterQueue: + Type: SQS + TargetArn: !GetAtt MyFunctionDLQ.Arn + + MyFunctionDLQ: + Type: AWS::SQS::Queue + Properties: + QueueName: MyFunction-DLQ + MessageRetentionPeriod: 1209600 # 14 days +``` + +**SQS event source**: + +```yaml title="template.yaml (SAM)" linenums="1" +Resources: + MyQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: MyQueue + RedrivePolicy: + deadLetterTargetArn: !GetAtt MyQueueDLQ.Arn + maxReceiveCount: 3 # Retry 3 times before DLQ + + MyQueueDLQ: + Type: AWS::SQS::Queue + Properties: + QueueName: MyQueue-DLQ +``` + +**Monitoring DLQs**: + +```yaml title="template.yaml (SAM)" linenums="1" +Resources: + DLQAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: MyFunction-DLQ-Alarm + MetricName: ApproximateNumberOfMessagesVisible + Namespace: AWS/SQS + Statistic: Sum + Period: 300 + EvaluationPeriods: 1 + Threshold: 1 + ComparisonOperator: GreaterThanOrEqualToThreshold + Dimensions: + - Name: QueueName + Value: !GetAtt MyFunctionDLQ.QueueName + AlarmActions: + - !Ref AlertTopic +``` + +--- + +## Best Practices + +### ✅ Do + +- **Use specific exception types** to distinguish error categories (validation, business logic, + infrastructure) +- **Log errors with context**: Request IDs, correlation IDs, input parameters (sanitized) +- **Respect cancellation tokens**: Pass to all async operations, check `IsCancellationRequested` in + loops +- **Implement retries for transient failures**: Network errors, timeouts, 5xx HTTP responses +- **Use middleware for global error handling**: Logging, metrics, error tracking service integration +- **Return structured error responses**: Consistent error format for API clients +- **Configure appropriate `InvocationCancellationBuffer`**: Balance between graceful handling and + Lambda timeout +- **Handle `OperationCanceledException` gracefully**: Return partial results or meaningful error + response +- **Monitor DLQs with CloudWatch alarms**: Get notified when messages accumulate +- **Use OnInit return value for graceful abort**: Return `false` instead of throwing when startup + should abort + +### ❌ Don't + +- **Don't swallow exceptions without logging**: Silent failures make debugging impossible +- **Don't ignore cancellation tokens**: Leads to hard Lambda timeouts and incomplete work +- **Don't retry non-transient errors**: Validation errors, authentication failures, 4xx responses +- **Don't let OnInit timeout**: Keep initialization fast (< 5 seconds by default) +- **Don't assume OnShutdown always completes**: AWS sends SIGKILL after ShutdownDuration +- **Don't use long shutdown operations**: Limited time window (typically < 500ms) +- **Don't log sensitive data**: Sanitize PII, credentials, and secrets before logging +- **Don't retry indefinitely**: Respect Lambda timeout, use exponential backoff with max retries + +--- + +## Anti-Patterns to Avoid + +### ❌ Ignoring Cancellation Tokens + +```csharp title="❌ Bad - No cancellation token" +lambda.MapHandler(async ([Event] Request request, IDataService dataService) => +{ + // This operation can't be cancelled - will continue until Lambda timeout + var data = await dataService.ProcessLargeDatasetAsync(request.DatasetId); + return new Response { Data = data }; +}); +``` + +```csharp title="✅ Good - Respects cancellation" +lambda.MapHandler(async ( + [Event] Request request, + IDataService dataService, + CancellationToken cancellationToken) => +{ + try + { + var data = await dataService.ProcessLargeDatasetAsync(request.DatasetId, cancellationToken); + return new Response { Data = data }; + } + catch (OperationCanceledException) + { + // Handle timeout gracefully + return new Response { Status = "Timeout", PartialData = dataService.GetPartialResults() }; + } +}); +``` + +### ❌ OnInit Timeout Loops + +```csharp title="❌ Bad - Infinite loop ignores timeout" +lambda.OnInit(async (CancellationToken cancellationToken) => +{ + while (true) // Will timeout after InitTimeout (default 5s) + { + await Task.Delay(100); + } + return true; +}); +``` + +```csharp title="✅ Good - Respects cancellation token" +lambda.OnInit(async (ICache cache, CancellationToken cancellationToken) => +{ + try + { + await cache.WarmUpAsync(cancellationToken); + return true; + } + catch (OperationCanceledException) + { + Console.WriteLine("Init timeout - cache warmup incomplete"); + return false; // Abort startup gracefully + } +}); +``` + +### ❌ Retrying Non-Transient Errors + +```csharp title="❌ Bad - Retries validation errors" +for (int i = 0; i < 3; i++) +{ + try + { + return await orderService.CreateOrderAsync(order); + } + catch (Exception) // Catches ALL exceptions including validation errors + { + await Task.Delay(1000); // Wastes time retrying invalid input + } +} +``` + +```csharp title="✅ Good - Only retries transient errors" +for (int i = 0; i < 3; i++) +{ + try + { + return await orderService.CreateOrderAsync(order); + } + catch (ValidationException) + { + throw; // Don't retry validation errors + } + catch (HttpRequestException) when (i < 2) + { + await Task.Delay(1000); // Retry transient network errors + } +} +``` + +--- + +## Troubleshooting + +### "CancellationTokenSource provided with insufficient time" + +**Error message**: + +``` +InvalidOperationException: CancellationTokenSource provided with insufficient time. +Lambda Remaining Time = 00:00:02, Cancellation Token Buffer = 00:00:03, Candidate Token Duration = -00:00:01 +``` + +**Cause**: `InvocationCancellationBuffer` exceeds Lambda's remaining time. + +**Solutions**: + +1. **Reduce the buffer**: + ```csharp + builder.Services.ConfigureLambdaHostOptions(options => + { + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(1); + }); + ``` + +2. **Increase Lambda timeout** (in SAM/CDK/Terraform template): + ```yaml + MyFunction: + Type: AWS::Serverless::Function + Properties: + Timeout: 30 # Increase from current value + ``` + +3. **Optimize previous operations** to leave more remaining time. + +### "Encountered errors while running OnInit handlers" + +**Error message**: + +``` +AggregateException: Encountered errors while running OnInit handlers: + ---> InvalidOperationException: Database connection failed + ---> TimeoutException: Cache warmup timed out +``` + +**Cause**: One or more OnInit handlers threw exceptions. + +**Solutions**: + +1. **Check inner exceptions**: + ```csharp + try + { + await lambdaHost.RunAsync(); + } + catch (AggregateException aggEx) + { + foreach (var innerEx in aggEx.InnerExceptions) + { + Console.WriteLine($"Init error: {innerEx.Message}"); + } + } + ``` + +2. **Add error handling to OnInit handlers**: + ```csharp + lambda.OnInit(async (ICache cache, CancellationToken ct) => + { + try + { + await cache.WarmUpAsync(ct); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Cache warmup failed: {ex.Message}"); + return false; // Abort gracefully instead of throwing + } + }); + ``` + +3. **Increase InitTimeout**: + ```csharp + builder.Services.ConfigureLambdaHostOptions(options => + { + options.InitTimeout = TimeSpan.FromSeconds(10); + }); + ``` + +### "Graceful shutdown of the Lambda function failed" + +**Error message**: + +``` +OperationCanceledException: Graceful shutdown of the Lambda function failed: the bootstrap operation did not complete within the allocated timeout period. +``` + +**Cause**: Bootstrap process didn't complete within shutdown timeout. + +**Solutions**: + +1. **Ensure OnShutdown handlers respect cancellation token**: + ```csharp + lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => + { + try + { + await metrics.FlushAsync(ct); // Must support cancellation + } + catch (OperationCanceledException) + { + Console.WriteLine("Metrics flush cancelled due to shutdown timeout"); + } + }); + ``` + +2. **Reduce OnShutdown work**: + ```csharp + lambda.OnShutdown(async (IMetrics metrics) => + { + // Quick flush - don't wait for acknowledgment + metrics.FlushAndForget(); + await Task.CompletedTask; + }); + ``` + +3. **Increase shutdown duration** (if using external extensions): + ```csharp + builder.Services.ConfigureLambdaHostOptions(options => + { + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500ms + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(50); + }); + ``` + +--- + +## Key Takeaways + +1. **Exception handling**: Use middleware for global error policies, handlers for specific error + responses +2. **Cancellation tokens**: Critical for respecting Lambda timeouts and graceful shutdown +3. **InvocationCancellationBuffer**: Prevents hard timeouts by firing cancellation before Lambda + terminates +4. **OnInit errors**: Handlers run in parallel, errors aggregated, can return `false` to abort +5. **OnShutdown errors**: All handlers execute despite individual failures, errors aggregated +6. **Retries**: Framework provides no built-in retry - use Polly or custom logic +7. **DLQs**: Configure at AWS Lambda level for asynchronous event sources +8. **Error logging**: Always include request context (request ID, correlation ID) + +--- + +## Next Steps + +- **[Testing](testing.md)** - Learn how to test error handling logic +- **[Lifecycle Management](lifecycle-management.md)** - Deep dive into OnInit and OnShutdown +- **[Configuration](configuration.md)** - Configure timeout and cancellation settings +- **[Deployment](deployment.md)** - Configure DLQs and CloudWatch alarms in infrastructure From 2ccd37795a912606ca33980d81b6b183ec146a96 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 10:37:48 -0500 Subject: [PATCH 09/65] feat(docs): update mkdocs.yml to enhance navigation and search features - Added new navigation options: tracking, toc.follow, and instant navigation support. - Integrated advanced search options: highlight, share, and suggest functionalities. - Removed unused `navigation.prune` for cleaner configuration. - Improved formatting and indentation for better readability. --- mkdocs.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 7a80b43d..74779bd3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,10 +22,15 @@ theme: - navigation.tabs.sticky - navigation.expand - navigation.path - - navigation.prune - navigation.indexes - - navigation.top - + - navigation.tracking + - toc.follow + - navigation.instant + - search.highlight + - search.share + - search.suggest + + palette: # Palette toggle for light mode - scheme: default @@ -76,11 +81,11 @@ watch: nav: - Home: index.md - Getting Started: - - getting-started/index.md - - Installation: getting-started/installation.md - - Your First Lambda: getting-started/first-lambda.md - - Core Concepts: getting-started/core-concepts.md - - Project Structure: getting-started/project-structure.md + - getting-started/index.md + - Installation: getting-started/installation.md + - Your First Lambda: getting-started/first-lambda.md + - Core Concepts: getting-started/core-concepts.md + - Project Structure: getting-started/project-structure.md - Guides: - guides/index.md - Dependency Injection: guides/dependency-injection.md From fdd402609863ecf274777de15903f563edbba876 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 11:51:48 -0500 Subject: [PATCH 10/65] feat(docs): improve readability and navigation, add changelog - Reformatted line breaks in index.md for better readability and consistency. - Enhanced table structures for package and envelope documentation. - Updated navigation in mkdocs.yml to support sub-navigation for "Home" and added "Changelog". - Included changelog.md for tracking version history and release notes. --- docs/changelog.md | 1 + docs/index.md | 86 +++++++++++++++++++++++------------------------ mkdocs.yml | 4 ++- 3 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 docs/changelog.md diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..90cb31c6 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 5d43c24e..982d3154 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,8 @@ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=j-d-ha_aws-lambda-host&metric=alert_status&token=9fb519975d91379dcfbc6c13a4bd4207131af6e3)](https://sonarcloud.io/summary/new_code?id=j-d-ha_aws-lambda-host) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/j-d-ha/aws-lambda-host/blob/main/LICENSE) -A modern .NET framework that brings familiar .NET Core patterns to AWS Lambda - middleware, dependency injection, lifecycle hooks, and async-first design. +A modern .NET framework that brings familiar .NET Core patterns to AWS Lambda - middleware, +dependency injection, lifecycle hooks, and async-first design. [Get Started](getting-started/){ .md-button .md-button--primary } [View Examples](examples/){ .md-button } @@ -105,7 +106,8 @@ Stop writing boilerplate Lambda code. Start building features with patterns you ### :material-view-dashboard-outline: .NET Hosting Patterns -Use middleware, builder pattern, and dependency injection similar to ASP.NET Core, with proper scoped lifetime management per invocation. +Use middleware, builder pattern, and dependency injection similar to ASP.NET Core, with proper +scoped lifetime management per invocation. [Learn about DI](guides/dependency-injection.md){ .md-button } @@ -117,7 +119,8 @@ Native support for async/await with proper Lambda timeout and cancellation handl ### :material-code-braces: Source Generators & Interceptors -Compile-time code generation and method interception for optimal performance with zero runtime reflection. +Compile-time code generation and method interception for optimal performance with zero runtime +reflection. [Explore advanced topics](advanced/source-generators.md){ .md-button } @@ -129,7 +132,8 @@ Full support for Ahead-of-Time compilation for faster cold starts and reduced me ### :material-chart-line: Built-in Observability -OpenTelemetry integration for distributed tracing with automatic root span creation and custom instrumentation. +OpenTelemetry integration for distributed tracing with automatic root span creation and custom +instrumentation. [OpenTelemetry setup](features/opentelemetry.md){ .md-button } @@ -159,38 +163,25 @@ Create your first Lambda handler: ```csharp using AwsLambda.Host.Builder; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; // Create the application builder var builder = LambdaApplication.CreateBuilder(); -// Register your services -builder.Services.AddScoped(); - // Build the Lambda application var lambda = builder.Build(); -// Map your handler - services are automatically injected -lambda.MapHandler(([Event] string input, IGreetingService greeting) - => greeting.Greet(input)); +// Map your handler - the event is automatically injected +lambda.MapHandler(([Event] string name) => $"Hello {name}!"); // Run the Lambda await lambda.RunAsync(); -// Define your service -public interface IGreetingService -{ - string Greet(string name); -} - -public class GreetingService : IGreetingService -{ - public string Greet(string name) => $"Hello {name}!"; -} ``` !!! tip "Next Steps" - Ready to dive deeper? Check out the [Getting Started Guide](getting-started/) for a complete tutorial, or explore the [Examples](examples/) to see real-world applications. +Ready to dive deeper? Check out the [Getting Started Guide](getting-started/) for a complete +tutorial, or explore the [Examples](examples/) to see real-world applications. --- @@ -198,33 +189,37 @@ public class GreetingService : IGreetingService ### Core Packages -The core packages provide the fundamental hosting framework, abstractions, and observability support for building AWS Lambda functions. +The core packages provide the fundamental hosting framework, abstractions, and observability support +for building AWS Lambda functions. -| Package | Description | NuGet | Downloads | -|---------|-------------|-------|-----------| -| [**AwsLambda.Host**](api-reference/host.md) | Core hosting framework with middleware and DI | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host/) | -| [**AwsLambda.Host.Abstractions**](api-reference/abstractions.md) | Core interfaces and contracts | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions/) | -| [**AwsLambda.Host.OpenTelemetry**](features/opentelemetry.md) | Distributed tracing and observability | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry/) | +| Package | Description | NuGet | Downloads | +|------------------------------------------------------------------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| +| [**AwsLambda.Host**](api-reference/host.md) | Core hosting framework with middleware and DI | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host/) | +| [**AwsLambda.Host.Abstractions**](api-reference/abstractions.md) | Core interfaces and contracts | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions/) | +| [**AwsLambda.Host.OpenTelemetry**](features/opentelemetry.md) | Distributed tracing and observability | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry/) | ### Envelope Packages -Envelope packages provide type-safe handling of AWS Lambda event sources with automatic payload deserialization. +Envelope packages provide type-safe handling of AWS Lambda event sources with automatic payload +deserialization. !!! info "What are Envelopes?" - Envelopes wrap AWS Lambda events with strongly-typed payload handling, giving you compile-time type safety and automatic deserialization of message bodies from SQS, SNS, Kinesis, and other event sources. +Envelopes wrap AWS Lambda events with strongly-typed payload handling, giving you compile-time type +safety and automatic deserialization of message bodies from SQS, SNS, Kinesis, and other event +sources. [Learn more about envelopes](features/envelopes/){ .md-button } -| Package | Description | NuGet | Downloads | -|---------|-------------|-------|-----------| -| [**AwsLambda.Host.Envelopes.Sqs**](features/envelopes/sqs.md) | Simple Queue Service events with typed message bodies | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs/) | -| [**AwsLambda.Host.Envelopes.Sns**](features/envelopes/sns.md) | Simple Notification Service messages | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns/) | -| [**AwsLambda.Host.Envelopes.ApiGateway**](features/envelopes/api-gateway.md) | REST, HTTP, and WebSocket APIs | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway/) | -| [**AwsLambda.Host.Envelopes.Kinesis**](features/envelopes/kinesis.md) | Data Streams with typed records | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis/) | -| [**AwsLambda.Host.Envelopes.KinesisFirehose**](features/envelopes/kinesis-firehose.md) | Data transformation | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose/) | -| [**AwsLambda.Host.Envelopes.Kafka**](features/envelopes/kafka.md) | MSK and self-managed Kafka | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka/) | -| [**AwsLambda.Host.Envelopes.CloudWatchLogs**](features/envelopes/cloudwatch-logs.md) | Log subscriptions | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs/) | -| [**AwsLambda.Host.Envelopes.Alb**](features/envelopes/alb.md) | Application Load Balancer requests | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb/) | +| Package | Description | NuGet | Downloads | +|----------------------------------------------------------------------------------------|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [**AwsLambda.Host.Envelopes.Sqs**](features/envelopes/sqs.md) | Simple Queue Service events with typed message bodies | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs/) | +| [**AwsLambda.Host.Envelopes.Sns**](features/envelopes/sns.md) | Simple Notification Service messages | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns/) | +| [**AwsLambda.Host.Envelopes.ApiGateway**](features/envelopes/api-gateway.md) | REST, HTTP, and WebSocket APIs | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway/) | +| [**AwsLambda.Host.Envelopes.Kinesis**](features/envelopes/kinesis.md) | Data Streams with typed records | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis/) | +| [**AwsLambda.Host.Envelopes.KinesisFirehose**](features/envelopes/kinesis-firehose.md) | Data transformation | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose/) | +| [**AwsLambda.Host.Envelopes.Kafka**](features/envelopes/kafka.md) | MSK and self-managed Kafka | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka/) | +| [**AwsLambda.Host.Envelopes.CloudWatchLogs**](features/envelopes/cloudwatch-logs.md) | Log subscriptions | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs/) | +| [**AwsLambda.Host.Envelopes.Alb**](features/envelopes/alb.md) | Application Load Balancer requests | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb/) | [Browse all envelope packages](features/envelopes/){ .md-button } @@ -237,7 +232,8 @@ Explore complete example projects demonstrating real-world Lambda patterns: - **[Hello World](examples/hello-world.md)** - Basic Lambda with dependency injection and middleware - **[REST API](examples/api-rest.md)** - API Gateway integration with request/response handling - **[SQS Processing](examples/sqs-processing.md)** - Event-driven message processing -- **[OpenTelemetry](examples/opentelemetry-example.md)** - Full observability with distributed tracing +- **[OpenTelemetry](examples/opentelemetry-example.md)** - Full observability with distributed + tracing - **[AOT Compilation](examples/aot-example.md)** - Native AOT for optimal cold start performance [View all examples](examples/){ .md-button } @@ -248,8 +244,9 @@ Explore complete example projects demonstrating real-world Lambda patterns: ### Get Involved -- **[GitHub Repository](https://github.com/j-d-ha/aws-lambda-host)** - Source code, issues, and discussions -- **[Changelog](resources/changelog.md)** - Version history and release notes +- **[GitHub Repository](https://github.com/j-d-ha/aws-lambda-host)** - Source code, issues, and + discussions +- **[Changelog](changelog.md)** - Version history and release notes - **[License](https://github.com/j-d-ha/aws-lambda-host/blob/main/LICENSE)** - MIT License ### Documentation @@ -270,4 +267,5 @@ Need help or want to contribute? --- -**Ready to modernize your Lambda development?** [Get started now](getting-started/){ .md-button .md-button--primary } +**Ready to modernize your Lambda development?** [Get started now](getting-started/){ .md-button +.md-button--primary } diff --git a/mkdocs.yml b/mkdocs.yml index 74779bd3..f179c8d4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -79,7 +79,9 @@ watch: - ./docs/ nav: - - Home: index.md + - Home: + - index.md + - Changelog: changelog.md - Getting Started: - getting-started/index.md - Installation: getting-started/installation.md From 2222a4c7da94536eb55c7056a8ace2a51c58f781 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 11:57:02 -0500 Subject: [PATCH 11/65] fix(tasks): update CleanupCode profile and reformat index.md - Changed CleanupCode profile in FormattingTasks.yml to "Full Cleanup Custom". - Adjusted formatting in docs/index.md for consistent indentation and spacing. --- tasks/FormattingTasks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/FormattingTasks.yml b/tasks/FormattingTasks.yml index b13262e0..d503097a 100644 --- a/tasks/FormattingTasks.yml +++ b/tasks/FormattingTasks.yml @@ -19,7 +19,7 @@ tasks: dotnet jb cleanupcode \ AwsLambda.Host.sln \ --settings="AwsLambda.Host.sln.DotSettings" \ - --profile="Built-in: Full Cleanup" \ + --profile="Full Cleanup Custom" \ --verbosity=INFO \ --no-build - echo "✅ CleanupCode completed successfully!" From c4f58b945775c77bb4afb8ac4710adb4b1ecd3ea Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 11:58:09 -0500 Subject: [PATCH 12/65] chore(settings): update code cleanup profile and reformat documentation - Updated `AwsLambda.Host.sln.DotSettings` with a new "Full Custom Cleanup" profile. - Reformatted indentation and spacing in `docs/index.md` for consistency. --- AwsLambda.Host.sln.DotSettings | 79 ++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/AwsLambda.Host.sln.DotSettings b/AwsLambda.Host.sln.DotSettings index 19d9e316..7d920dee 100644 --- a/AwsLambda.Host.sln.DotSettings +++ b/AwsLambda.Host.sln.DotSettings @@ -1,4 +1,83 @@  + <?xml version="1.0" encoding="utf-16"?><Profile name="Full Custom Cleanup"><CppReformatCode>True</CppReformatCode><FSharpReformatCode>True</FSharpReformatCode><ShaderLabReformatCode>True</ShaderLabReformatCode><XMLReformatCode>True</XMLReformatCode><VBReformatCode>True</VBReformatCode><CSReformatCode>True</CSReformatCode><CSharpReformatComments>True</CSharpReformatComments><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" ArrangeArgumentsStyle="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" ArrangeNullCheckingPattern="True" /><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CppCodeStyleCleanupDescriptor ArrangeBraces="True" ArrangeAuto="True" ArrangeFunctionDeclarations="True" ArrangeNestedNamespaces="True" ArrangeTypeAliases="True" ArrangeCVQualifiers="True" ArrangeSlashesInIncludeDirectives="True" ArrangeOverridingFunctions="True" SortDefinitions="True" SortIncludeDirectives="True" SortMemberInitializers="True" /><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>True</EmbraceInRegion></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><Xaml.RemoveRedundantNamespaceAlias>True</Xaml.RemoveRedundantNamespaceAlias><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CSReorderTypeMembers>True</CSReorderTypeMembers><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CppAddTypenameTemplateKeywords>True</CppAddTypenameTemplateKeywords><CppCStyleToStaticCastDescriptor>True</CppCStyleToStaticCastDescriptor><CppRedundantDereferences>True</CppRedundantDereferences><CppDeleteRedundantAccessSpecifier>True</CppDeleteRedundantAccessSpecifier><CppRemoveCastDescriptor>True</CppRemoveCastDescriptor><CppRemoveElseKeyword>True</CppRemoveElseKeyword><CppShortenQualifiedName>True</CppShortenQualifiedName><CppDeleteRedundantSpecifier>True</CppDeleteRedundantSpecifier><CppRemoveStatement>True</CppRemoveStatement><CppDeleteRedundantTypenameTemplateKeywords>True</CppDeleteRedundantTypenameTemplateKeywords><CppReplaceExpressionWithBooleanConst>True</CppReplaceExpressionWithBooleanConst><CppMakeIfConstexpr>True</CppMakeIfConstexpr><CppMakePostfixOperatorPrefix>True</CppMakePostfixOperatorPrefix><CppMakeVariableConstexpr>True</CppMakeVariableConstexpr><CppChangeSmartPointerToMakeFunction>True</CppChangeSmartPointerToMakeFunction><CppReplaceThrowWithRethrowFix>True</CppReplaceThrowWithRethrowFix><CppTypeTraitAliasDescriptor>True</CppTypeTraitAliasDescriptor><CppRemoveRedundantConditionalExpressionDescriptor>True</CppRemoveRedundantConditionalExpressionDescriptor><CppSimplifyConditionalExpressionDescriptor>True</CppSimplifyConditionalExpressionDescriptor><CppReplaceExpressionWithNullptr>True</CppReplaceExpressionWithNullptr><CppReplaceTieWithStructuredBindingDescriptor>True</CppReplaceTieWithStructuredBindingDescriptor><CppUseAssociativeContainsDescriptor>True</CppUseAssociativeContainsDescriptor><CppUseEraseAlgorithmDescriptor>True</CppUseEraseAlgorithmDescriptor><CppJoinDeclarationAndAssignmentDescriptor>True</CppJoinDeclarationAndAssignmentDescriptor><CppMakeClassFinal>True</CppMakeClassFinal><CppMakeLocalVarConstDescriptor>True</CppMakeLocalVarConstDescriptor><CppMakeMethodConst>True</CppMakeMethodConst><CppMakeMethodStatic>True</CppMakeMethodStatic><CppMakePtrOrRefParameterConst>True</CppMakePtrOrRefParameterConst><CppMakeParameterConst>True</CppMakeParameterConst><CppPassValueParameterByConstReference>True</CppPassValueParameterByConstReference><CppRemoveElaboratedTypeSpecifierDescriptor>True</CppRemoveElaboratedTypeSpecifierDescriptor><CppRemoveRedundantLambdaParameterListDescriptor>True</CppRemoveRedundantLambdaParameterListDescriptor><CppRemoveRedundantMemberInitializerDescriptor>True</CppRemoveRedundantMemberInitializerDescriptor><CppRemoveRedundantParentheses>True</CppRemoveRedundantParentheses><CppRemoveTemplateArgumentsDescriptor>True</CppRemoveTemplateArgumentsDescriptor><CppRemoveUnreachableCode>True</CppRemoveUnreachableCode><CppRemoveUnusedIncludes>True</CppRemoveUnusedIncludes><CppRemoveUnusedLambdaCaptures>True</CppRemoveUnusedLambdaCaptures><CppReplaceIfWithIfConsteval>True</CppReplaceIfWithIfConsteval><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><VBMakeFieldReadonly>True</VBMakeFieldReadonly><Xaml.RedundantFreezeAttribute>True</Xaml.RedundantFreezeAttribute><Xaml.RemoveRedundantModifiersAttribute>True</Xaml.RemoveRedundantModifiersAttribute><Xaml.RemoveRedundantNameAttribute>True</Xaml.RemoveRedundantNameAttribute><Xaml.RemoveRedundantResource>True</Xaml.RemoveRedundantResource><Xaml.RemoveRedundantCollectionProperty>True</Xaml.RemoveRedundantCollectionProperty><Xaml.RemoveRedundantAttachedPropertySetter>True</Xaml.RemoveRedundantAttachedPropertySetter><Xaml.RemoveRedundantStyledValue>True</Xaml.RemoveRedundantStyledValue><Xaml.RemoveForbiddenResourceName>True</Xaml.RemoveForbiddenResourceName><Xaml.RemoveRedundantGridDefinitionsAttribute>True</Xaml.RemoveRedundantGridDefinitionsAttribute><Xaml.RemoveRedundantUpdateSourceTriggerAttribute>True</Xaml.RemoveRedundantUpdateSourceTriggerAttribute><Xaml.RemoveRedundantBindingModeAttribute>True</Xaml.RemoveRedundantBindingModeAttribute><Xaml.RemoveRedundantGridSpanAttribut>True</Xaml.RemoveRedundantGridSpanAttribut><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="Full Custom Cleanup" /&gt; + &lt;inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="JSRemoveUnnecessaryParentheses" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="UnterminatedStatementJS" enabled="true" level="WARNING" enabled_by_default="true" /&gt; +&lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id="CSS"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HCL"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="HTTP Request"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Handlebars"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Ini"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Jade"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JavaScript"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="Markdown"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Mermaid"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="PowerShell"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Properties"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Razor"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="SQL"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="TOML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="VueExpr"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="yaml"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; +&lt;/profile&gt;</RIDER_SETTINGS></Profile> Built-in: Full Cleanup NotRequired NotRequired From cafee8edebb430d77eb7b524c769f93b3a7cfefd Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 11:58:49 -0500 Subject: [PATCH 13/65] feat(docs, examples): reformat docs and simplify Lambda example - Corrected inconsistent indentation in docs/index.md for better readability and uniformity. - Simplified the AWS Lambda example in Program.cs for clarity and ease of understanding. --- docs/index.md | 10 ++--- .../Program.cs | 40 +++++-------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/docs/index.md b/docs/index.md index 982d3154..321132b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -180,8 +180,8 @@ await lambda.RunAsync(); ``` !!! tip "Next Steps" -Ready to dive deeper? Check out the [Getting Started Guide](getting-started/) for a complete -tutorial, or explore the [Examples](examples/) to see real-world applications. + Ready to dive deeper? Check out the [Getting Started Guide](getting-started/) for a complete + tutorial, or explore the [Examples](examples/) to see real-world applications. --- @@ -204,9 +204,9 @@ Envelope packages provide type-safe handling of AWS Lambda event sources with au deserialization. !!! info "What are Envelopes?" -Envelopes wrap AWS Lambda events with strongly-typed payload handling, giving you compile-time type -safety and automatic deserialization of message bodies from SQS, SNS, Kinesis, and other event -sources. + Envelopes wrap AWS Lambda events with strongly-typed payload handling, giving you compile-time type + safety and automatic deserialization of message bodies from SQS, SNS, Kinesis, and other event + sources. [Learn more about envelopes](features/envelopes/){ .md-button } diff --git a/examples/AwsLambda.Host.Example.HelloWorld/Program.cs b/examples/AwsLambda.Host.Example.HelloWorld/Program.cs index 86b430bd..e70d854e 100644 --- a/examples/AwsLambda.Host.Example.HelloWorld/Program.cs +++ b/examples/AwsLambda.Host.Example.HelloWorld/Program.cs @@ -1,40 +1,18 @@ -using System; -using System.Text.Json.Serialization; +#region + using AwsLambda.Host.Builder; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -var builder = LambdaApplication.CreateBuilder(); +#endregion -builder.Services.AddSingleton(); +// Create the application builder +var builder = LambdaApplication.CreateBuilder(); +// Build the Lambda application var lambda = builder.Build(); -lambda.UseMiddleware( - async (context, next) => - { - Console.WriteLine("[Middleware 1]: Before"); - await next(context); - Console.WriteLine("[Middleware 1]: After"); - } -); - -lambda.MapHandler( - ([Event] Request request, IService service) => new Response(service.GetMessage(request.Name)) -); +// Map your handler - the event is automatically injected +lambda.MapHandler(([Event] string name) => $"Hello {name}!"); +// Run the Lambda await lambda.RunAsync(); - -internal record Response([property: JsonPropertyName("message")] string Message); - -internal record Request([property: JsonPropertyName("name")] string Name); - -internal interface IService -{ - string GetMessage(string name); -} - -internal class Service : IService -{ - public string GetMessage(string name) => $"hello {name}"; -} From 352c6ba7380aaa4ccf4ecb5f363530a9a83019de Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 12:22:59 -0500 Subject: [PATCH 14/65] feat(docs): simplify description and update lifecycle management section - Removed "async-first design" mention for concise framework description. - Updated section title to "Lifecycle Management" with corresponding icon change. - Minor adjustments to streamline content and improve clarity. --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 321132b8..c84dda6b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/j-d-ha/aws-lambda-host/blob/main/LICENSE) A modern .NET framework that brings familiar .NET Core patterns to AWS Lambda - middleware, -dependency injection, lifecycle hooks, and async-first design. +dependency injection, and lifecycle hooks. [Get Started](getting-started/){ .md-button .md-button--primary } [View Examples](examples/){ .md-button } @@ -111,7 +111,7 @@ scoped lifetime management per invocation. [Learn about DI](guides/dependency-injection.md){ .md-button } -### :material-lightning-bolt-outline: Async-First Design +### :material-calendar-sync-outline: Lifecycle Management Native support for async/await with proper Lambda timeout and cancellation handling built-in. From bd87661dda3e57daebe9ad2e62bb02cbb6620925 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 13:15:00 -0500 Subject: [PATCH 15/65] feat(docs): add deployment and testing guides - Introduced a detailed deployment guide for AWS Lambda, covering SAM, CDK, and Terraform. - Added instructions for CI/CD integration using GitHub Actions with SAM and Native AOT workflows. - Documented best practices, troubleshooting, and monitoring strategies for Lambda deployment. - Created a placeholder for testing guide, with plans to cover unit, integration, and lifecycle testing. - Included references to existing test patterns, examples, and tools for ongoing development. --- docs/guides/deployment.md | 814 ++++++++++++++++++++++++++++++++++++++ docs/guides/testing.md | 61 +++ 2 files changed, 875 insertions(+) create mode 100644 docs/guides/deployment.md create mode 100644 docs/guides/testing.md diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md new file mode 100644 index 00000000..d2da865e --- /dev/null +++ b/docs/guides/deployment.md @@ -0,0 +1,814 @@ +# Deployment + +This guide covers deploying AWS Lambda functions built with aws-lambda-host using various Infrastructure as Code (IaC) tools and CI/CD pipelines. + +--- + +## Deployment Options + +You can deploy Lambda functions using: + +- **AWS SAM (Serverless Application Model)** - AWS-native IaC framework optimized for serverless +- **AWS CDK (Cloud Development Kit)** - Type-safe infrastructure using C# +- **Terraform** - Cloud-agnostic IaC tool +- **Manual Deployment** - AWS CLI or Console +- **CI/CD Pipelines** - Automated deployment with GitHub Actions, GitLab CI, or Azure DevOps + +--- + +## Project Configuration + +### Standard Lambda (.csproj) + +For traditional Lambda functions with JIT compilation: + +```xml title="MyLambda.csproj" linenums="1" + + + Exe + net8.0 + enable + enable + + + Lambda + true + true + + + $(InterceptorsNamespaces);AwsLambda.Host + + + + + + + +``` + +**Build and publish**: + +```bash +dotnet publish -c Release -o ./publish +``` + +### Native AOT Lambda (.csproj) + +For optimized Lambda functions with Native AOT (faster cold starts, smaller package size): + +```xml title="MyLambdaAot.csproj" linenums="1" + + + Exe + net8.0 + enable + enable + + + true + true + false + full + true + + + bootstrap + + + Lambda + true + + + $(InterceptorsNamespaces);AwsLambda.Host + + + + + + + +``` + +**Build and publish for AOT**: + +```bash +# Publish with Native AOT (requires Docker for cross-compilation on non-Linux) +dotnet publish -c Release -o ./publish /p:PublishAot=true + +# Or use the AWS Lambda build container +docker run --rm -v $(pwd):/var/task public.ecr.aws/sam/build-dotnet8:latest \ + dotnet publish -c Release -o /var/task/publish /p:PublishAot=true +``` + +!!! tip "Native AOT Benefits" + - **Faster cold starts**: 50-80% reduction in cold start times + - **Smaller package size**: Trimming removes unused code + - **Lower memory usage**: More efficient resource utilization + - **Predictable performance**: No JIT compilation overhead + +--- + +## AWS SAM Deployment + +AWS SAM provides the simplest deployment experience for Lambda functions. + +### SAM Template + +Create a `template.yaml` file in your project root: + +```yaml title="template.yaml" linenums="1" +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: My Lambda Function + +Globals: + Function: + Timeout: 30 + MemorySize: 512 + Runtime: provided.al2023 + Architectures: + - x86_64 + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: bootstrap + CodeUri: ./publish + Environment: + Variables: + ENVIRONMENT: production + LOG_LEVEL: info + Events: + ApiEvent: + Type: Api + Properties: + Path: /orders + Method: post + +Outputs: + MyFunctionArn: + Description: Lambda Function ARN + Value: !GetAtt MyFunction.Arn + ApiUrl: + Description: API Gateway endpoint + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/orders" +``` + +### SAM Commands + +```bash title="Deployment commands" +# Build the Lambda function +dotnet publish -c Release -o ./publish + +# Package and deploy (first time - guided) +sam deploy --guided + +# Subsequent deployments +sam deploy + +# Delete the stack +sam delete +``` + +**sam deploy --guided** prompts: + +``` +Stack Name: my-lambda-stack +AWS Region: us-east-1 +Confirm changes before deploy: Y +Allow SAM CLI IAM role creation: Y +Disable rollback: N +Save arguments to configuration file: Y +SAM configuration file: samconfig.toml +``` + +### SAM with Native AOT + +```yaml title="template.yaml (AOT)" linenums="1" +Resources: + MyFunctionAot: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: dotnet8 + BuildArchitecture: x86_64 + Properties: + Handler: bootstrap + Runtime: provided.al2023 + CodeUri: ./ + Architectures: + - x86_64 + MemorySize: 512 + Timeout: 30 +``` + +```bash +# SAM builds with Docker automatically +sam build + +# Deploy +sam deploy --guided +``` + +--- + +## AWS CDK Deployment + +AWS CDK allows you to define infrastructure using C#. + +### CDK Stack + +```csharp title="MyLambdaStack.cs" linenums="1" +using Amazon.CDK; +using Amazon.CDK.AWS.Lambda; +using Amazon.CDK.AWS.APIGateway; +using Constructs; + +namespace MyCdkApp +{ + public class MyLambdaStack : Stack + { + public MyLambdaStack(Construct scope, string id, IStackProps props = null) + : base(scope, id, props) + { + // Lambda function with Native AOT + var myFunction = new Function(this, "MyFunction", new FunctionProps + { + Runtime = Runtime.PROVIDED_AL2023, + Handler = "bootstrap", + Code = Code.FromAsset("../MyLambda/publish"), + MemorySize = 512, + Timeout = Duration.Seconds(30), + Architecture = Architecture.X86_64, + Environment = new Dictionary + { + ["ENVIRONMENT"] = "production", + ["LOG_LEVEL"] = "info" + } + }); + + // API Gateway + var api = new RestApi(this, "MyApi", new RestApiProps + { + RestApiName = "My API", + Description = "API for My Lambda Function" + }); + + var integration = new LambdaIntegration(myFunction); + var orders = api.Root.AddResource("orders"); + orders.AddMethod("POST", integration); + + // Outputs + new CfnOutput(this, "FunctionArn", new CfnOutputProps + { + Value = myFunction.FunctionArn, + Description = "Lambda Function ARN" + }); + + new CfnOutput(this, "ApiUrl", new CfnOutputProps + { + Value = api.Url, + Description = "API Gateway URL" + }); + } + } +} +``` + +### CDK Program + +```csharp title="Program.cs" linenums="1" +using Amazon.CDK; + +namespace MyCdkApp +{ + class Program + { + static void Main(string[] args) + { + var app = new App(); + + new MyLambdaStack(app, "MyLambdaStack", new StackProps + { + Env = new Amazon.CDK.Environment + { + Account = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_ACCOUNT"), + Region = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_REGION") + } + }); + + app.Synth(); + } + } +} +``` + +### CDK Commands + +```bash title="CDK deployment" +# Install CDK CLI +npm install -g aws-cdk + +# Initialize CDK project (one time) +mkdir cdk && cd cdk +cdk init app --language csharp + +# Build Lambda function +cd ../MyLambda +dotnet publish -c Release -o ./publish + +# Deploy +cd ../cdk +cdk deploy + +# Destroy +cdk destroy +``` + +--- + +## Terraform Deployment + +Terraform provides cloud-agnostic infrastructure management. + +### Terraform Configuration + +```hcl title="main.tf" linenums="1" +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +# Lambda IAM Role +resource "aws_iam_role" "lambda_role" { + name = "my-lambda-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "lambda_basic" { + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.lambda_role.name +} + +# Lambda Function +resource "aws_lambda_function" "my_function" { + filename = "../MyLambda/publish.zip" + function_name = "my-lambda-function" + role = aws_iam_role.lambda_role.arn + handler = "bootstrap" + runtime = "provided.al2023" + architectures = ["x86_64"] + memory_size = 512 + timeout = 30 + + source_code_hash = filebase64sha256("../MyLambda/publish.zip") + + environment { + variables = { + ENVIRONMENT = "production" + LOG_LEVEL = "info" + } + } +} + +# API Gateway +resource "aws_apigatewayv2_api" "api" { + name = "my-api" + protocol_type = "HTTP" +} + +resource "aws_apigatewayv2_integration" "lambda_integration" { + api_id = aws_apigatewayv2_api.api.id + integration_type = "AWS_PROXY" + integration_uri = aws_lambda_function.my_function.invoke_arn +} + +resource "aws_apigatewayv2_route" "post_orders" { + api_id = aws_apigatewayv2_api.api.id + route_key = "POST /orders" + target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" +} + +resource "aws_apigatewayv2_stage" "default" { + api_id = aws_apigatewayv2_api.api.id + name = "$default" + auto_deploy = true +} + +resource "aws_lambda_permission" "api_gateway" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.my_function.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*" +} + +# Outputs +output "function_arn" { + value = aws_lambda_function.my_function.arn +} + +output "api_url" { + value = aws_apigatewayv2_stage.default.invoke_url +} +``` + +```hcl title="variables.tf" linenums="1" +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} +``` + +### Terraform Commands + +```bash title="Terraform deployment" +# Build and package Lambda +cd MyLambda +dotnet publish -c Release -o ./publish +cd publish && zip -r ../publish.zip . && cd .. + +# Initialize Terraform +cd ../terraform +terraform init + +# Plan deployment +terraform plan + +# Apply deployment +terraform apply + +# Destroy +terraform destroy +``` + +--- + +## CI/CD with GitHub Actions + +Automate builds and deployments using GitHub Actions. + +### GitHub Actions Workflow (SAM) + +```yaml title=".github/workflows/deploy.yml" linenums="1" +name: Deploy Lambda + +on: + push: + branches: + - main + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Build Lambda + run: | + cd src/MyLambda + dotnet publish -c Release -o ./publish + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsRole + aws-region: us-east-1 + + - name: Setup SAM + uses: aws-actions/setup-sam@v2 + + - name: Deploy with SAM + run: | + sam deploy --no-confirm-changeset --no-fail-on-empty-changeset +``` + +### GitHub Actions Workflow (Native AOT with Docker) + +```yaml title=".github/workflows/deploy-aot.yml" linenums="1" +name: Deploy Lambda (AOT) + +on: + push: + branches: + - main + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Lambda with Native AOT + run: | + docker run --rm \ + -v $(pwd)/src/MyLambda:/var/task \ + -v $(pwd)/publish:/var/task/publish \ + public.ecr.aws/sam/build-dotnet8:latest \ + dotnet publish -c Release -o /var/task/publish /p:PublishAot=true + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsRole + aws-region: us-east-1 + + - name: Setup SAM + uses: aws-actions/setup-sam@v2 + + - name: Deploy with SAM + run: | + sam deploy --no-confirm-changeset --no-fail-on-empty-changeset +``` + +### CDK Deployment with GitHub Actions + +```yaml title=".github/workflows/cdk-deploy.yml" linenums="1" +name: CDK Deploy + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Build Lambda + run: | + cd src/MyLambda + dotnet publish -c Release -o ./publish + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: us-east-1 + + - name: Install CDK + run: npm install -g aws-cdk + + - name: Deploy CDK Stack + run: | + cd cdk + dotnet build + cdk deploy --require-approval never +``` + +--- + +## Manual Deployment + +### Using AWS CLI + +```bash title="Manual deployment with AWS CLI" +# Build Lambda +dotnet publish -c Release -o ./publish + +# Package +cd publish && zip -r ../function.zip . && cd .. + +# Create function (first time) +aws lambda create-function \ + --function-name my-lambda-function \ + --runtime provided.al2023 \ + --role arn:aws:iam::ACCOUNT_ID:role/lambda-role \ + --handler bootstrap \ + --zip-file fileb://function.zip \ + --architectures x86_64 \ + --memory-size 512 \ + --timeout 30 + +# Update function code +aws lambda update-function-code \ + --function-name my-lambda-function \ + --zip-file fileb://function.zip +``` + +--- + +## Environment-Specific Configuration + +### Using appsettings.{Environment}.json + +```json title="appsettings.Production.json" +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "MyApp": { + "DatabaseConnectionString": "production-connection-string", + "CacheEnabled": true + } +} +``` + +**Set environment in Lambda**: + +```yaml title="template.yaml" +Environment: + Variables: + ASPNETCORE_ENVIRONMENT: Production +``` + +### Using AWS Systems Manager Parameter Store + +```csharp title="Program.cs" linenums="1" +using Amazon.Extensions.NETCore.Setup; +using Amazon.SimpleSystemsManagement; +using Amazon.SimpleSystemsManagement.Model; + +var builder = LambdaApplication.CreateBuilder(args); + +// Add AWS Systems Manager configuration +var ssmClient = new AmazonSimpleSystemsManagementClient(); +var parameter = await ssmClient.GetParameterAsync(new GetParameterRequest +{ + Name = "/myapp/database-connection-string", + WithDecryption = true +}); + +builder.Configuration.AddInMemoryCollection(new Dictionary +{ + ["ConnectionStrings:Default"] = parameter.Parameter.Value +}); + +var lambda = builder.Build(); +// ... +``` + +--- + +## Monitoring and Observability + +### CloudWatch Logs + +Lambda automatically sends logs to CloudWatch Logs. View logs: + +```bash +# Tail logs in real-time +sam logs --tail --name MyFunction + +# Or with AWS CLI +aws logs tail /aws/lambda/my-lambda-function --follow +``` + +### CloudWatch Metrics + +Monitor Lambda performance: + +```yaml title="template.yaml (SAM)" +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + # ... other properties + + # Alarm for errors + ErrorAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: !Sub "${MyFunction}-errors" + MetricName: Errors + Namespace: AWS/Lambda + Statistic: Sum + Period: 300 + EvaluationPeriods: 1 + Threshold: 5 + ComparisonOperator: GreaterThanThreshold + Dimensions: + - Name: FunctionName + Value: !Ref MyFunction +``` + +### OpenTelemetry Integration + +```bash +dotnet add package AwsLambda.Host.OpenTelemetry +``` + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.OpenTelemetry; + +var builder = LambdaApplication.CreateBuilder(args); + +// Add OpenTelemetry tracing +builder.Services.AddLambdaOpenTelemetry(); + +var lambda = builder.Build(); +// ... +``` + +--- + +## Best Practices + +### ✅ Do + +- **Use Native AOT for production** - Faster cold starts and lower costs +- **Set appropriate memory and timeout** - Right-size based on profiling +- **Use environment variables for configuration** - Avoid hardcoding values +- **Implement health checks** - Monitor Lambda function health +- **Use blue/green deployments** - Zero-downtime deployments with SAM or CDK +- **Monitor with CloudWatch** - Set up alarms for errors and performance +- **Version your functions** - Use aliases for staged rollouts +- **Enable X-Ray tracing** - Understand performance bottlenecks +- **Use Parameter Store or Secrets Manager** - Secure configuration management +- **Automate deployments with CI/CD** - Consistent, repeatable deployments + +### ❌ Don't + +- **Don't hardcode secrets** - Use Secrets Manager or Parameter Store +- **Don't over-provision memory** - Start small and increase based on metrics +- **Don't deploy without testing** - Use sam local or integration tests +- **Don't ignore cold starts** - Use provisioned concurrency or AOT compilation +- **Don't skip monitoring** - Always configure CloudWatch alarms +- **Don't use default timeouts** - Tune based on actual execution time +- **Don't deploy directly to production** - Use staging environments + +--- + +## Versioning + +The current version of aws-lambda-host is **1.0.1-beta.5** (from `Directory.Build.props`). + +```xml +1.0.1-beta.5 +``` + +Update package references: + +```bash +dotnet add package AwsLambda.Host --version 1.0.1-beta.5 +``` + +--- + +## Key Takeaways + +1. **Native AOT recommended** - Use for fastest cold starts and lowest costs +2. **Multiple deployment options** - SAM for simplicity, CDK for type safety, Terraform for multi-cloud +3. **CI/CD essential** - Automate builds and deployments with GitHub Actions +4. **Right-size resources** - Profile and adjust memory/timeout settings +5. **Monitor everything** - CloudWatch Logs, Metrics, and X-Ray tracing +6. **Secure configuration** - Use Parameter Store or Secrets Manager for sensitive data +7. **Infrastructure as Code** - Never manually configure Lambda in the console +8. **Environment separation** - Use separate stacks for dev/staging/production + +--- + +## Next Steps + +- **[Configuration](configuration.md)** - Configure Lambda host options +- **[Error Handling](error-handling.md)** - Handle errors and configure DLQs +- **[Testing](testing.md)** - Test Lambda functions before deployment +- **[OpenTelemetry Package](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/AwsLambda.Host.OpenTelemetry)** - Add distributed tracing diff --git a/docs/guides/testing.md b/docs/guides/testing.md new file mode 100644 index 00000000..adf46e80 --- /dev/null +++ b/docs/guides/testing.md @@ -0,0 +1,61 @@ +# Testing + +!!! info "Coming Soon" + This guide is currently under development. Check back soon for comprehensive testing documentation covering: + + - Unit testing Lambda handlers + - Testing with xUnit, NSubstitute, and AutoFixture + - Testing middleware components + - Testing lifecycle handlers (OnInit, OnShutdown) + - Integration testing strategies + - Mocking AWS Lambda context + - Best practices and patterns + +--- + +## Temporary Resources + +While this guide is being developed, you can refer to: + +- **[CONTRIBUTING.md](https://github.com/j-d-ha/aws-lambda-host/blob/main/CONTRIBUTING.md)** - Testing patterns and conventions used in the project +- **[Example test files](https://github.com/j-d-ha/aws-lambda-host/tree/main/tests/AwsLambda.Host.UnitTests)** - Real test examples from the framework +- **[AutoNSubstituteData pattern](https://github.com/j-d-ha/aws-lambda-host/blob/main/tests/AwsLambda.Host.UnitTests/AutoNSubstituteDataAttribute.cs)** - Test data generation attribute + +--- + +## Quick Testing Example + +```csharp title="OrderServiceTests.cs" linenums="1" +using NSubstitute; +using Xunit; + +public class OrderServiceTests +{ + [Fact] + public async Task ProcessAsync_ValidOrder_ReturnsSuccess() + { + // Arrange + var repository = Substitute.For(); + repository.SaveAsync(Arg.Any()) + .Returns(new SaveResult { Success = true }); + + var service = new OrderService(repository); + var order = new Order("123", 99.99m); + + // Act + var result = await service.ProcessAsync(order); + + // Assert + Assert.True(result.Success); + await repository.Received(1).SaveAsync(order); + } +} +``` + +--- + +## Next Steps + +- **[Deployment](deployment.md)** - Deploy your tested Lambda functions +- **[Error Handling](error-handling.md)** - Test error scenarios +- **[Handler Registration](handler-registration.md)** - Understand handler patterns for testing From 3b13c860809ad51ed6be88ec238d1673c081d8a7 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 13:18:16 -0500 Subject: [PATCH 16/65] docs(claude): remove outdated workflow and extension members guide - Deleted deprecated 'MANDATORY WORKFLOW' section from CLAUDE.md. - Removed obsolete C# 14 extension member syntax guide for clarity. - Retained relevant modern syntax instructions for extension members. --- CLAUDE.md | 109 ------------------------------------------------------ 1 file changed, 109 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index df0244d5..c526c876 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,59 +138,6 @@ extension(ref int number) This is modern C# 14. Leave it as-is. -## MANDATORY WORKFLOW - -### Task Classification - -- **Simple tasks**: Single file edits <20 lines, docs, comments, formatting -- **Complex tasks**: Multi-file changes, new features, architecture changes - -### Required Process - -#### 1. Planning (MANDATORY) - -- **MUST create PLAN.md** before starting any task -- Break down into numbered steps and sub-steps -- No implementation until plan approved - -#### 2. Execution - -- **Simple tasks**: Execute with minimal check-ins -- **Complex tasks**: Complete ONE step at a time, wait for user approval before proceeding -- **NEVER** continue to next step without explicit user sign-off - -#### 3. Git Workflow - -- Commit only after user approval -- Use clear commit messages -- Create feature branches for substantial changes -- Use conventional commits format: - - ```text - [optional scope]: - - [optional body] - - [optional footer(s)] - ``` - - Types: feat, fix, docs, refactor, test, chore, ci - -#### 4. Plan Template - -```markdown -# Task: [Description] - -## Objective -[What needs to be accomplished] - -## Steps -1. [Main step 1] - - [Sub-step 1.1] -2. [Main step 2] - -## Deliverables -- [Expected output] -``` - ## Release & Versioning Workflow ### Commit Message Requirements @@ -283,59 +230,3 @@ When a release is published, the GitHub release description is updated with the - **DO NOT** manually bump versions in Directory.Build.props (automatic) - **DO NOT** publish to NuGet manually (automated on release) - **DO NOT** create GitHub releases directly (use Release Drafter) - -# C# 14 Extension Members - Valid Syntax - -## This is VALID C# 14 syntax - do NOT change it - -C# 14 introduces **extension blocks** using the `extension` keyword. This is NEW, CORRECT syntax. - -## Valid Extension Block Syntax - -```csharp -public static class MyExtensions -{ - extension(string str) - { - public int WordCount() => str.Split().Length; - - public string FirstWord => str.Split().FirstOrDefault() ?? ""; - } -} -``` - -This is equivalent to the old syntax but supports properties and operators too: - -```csharp -// Old syntax (still valid) -public static class MyExtensions -{ - public static int WordCount(this string str) => str.Split().Length; -} -``` - -## Key Rules - -1. Extension blocks go inside **static classes** -2. Use `extension(Type receiver)` syntax -3. Members inside access `receiver` directly (no `this.`) -4. Supports: methods, properties, operators -5. Can use `extension(ref Type receiver)` for value types - -## For Ref Extensions - -```csharp -extension(ref int number) -{ - public void Increment() => number++; -} -``` - -## Do NOT Convert - -- ✅ `extension(string str)` is CORRECT -- ❌ Do NOT change to `this string str` -- ❌ Do NOT suggest "fixing" this syntax -- ❌ Do NOT flag as an error - -This is modern C# 14. Leave it as-is. \ No newline at end of file From 4635434be332626429064225e9cfdd61faf47c61 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 13:25:04 -0500 Subject: [PATCH 17/65] feat(docs): add features section with envelopes and OpenTelemetry guides - Introduced a dedicated "Features" section to showcase framework extensions and capabilities. - Added extensive documentation for envelopes, covering supported event sources like SQS, SNS, Kinesis, and more. - Included an in-depth guide for OpenTelemetry integration, with examples for tracing and metrics setup. - Updated mkdocs.yml to include navigation for envelopes, OpenTelemetry, and custom serialization. - Enhanced navigation for quick access to envelope patterns, observability, and related guides. --- docs/features/envelopes/sqs.md | 709 +++++++++++++++++++++++++++++++++ docs/features/index.md | 241 +++++++++++ mkdocs.yml | 14 + 3 files changed, 964 insertions(+) create mode 100644 docs/features/envelopes/sqs.md create mode 100644 docs/features/index.md diff --git a/docs/features/envelopes/sqs.md b/docs/features/envelopes/sqs.md new file mode 100644 index 00000000..af996c9f --- /dev/null +++ b/docs/features/envelopes/sqs.md @@ -0,0 +1,709 @@ +# SQS Envelope + +Strongly-typed SQS event handling with automatic JSON deserialization of message bodies. + +--- + +## Introduction + +The `AwsLambda.Host.Envelopes.Sqs` package provides type-safe wrappers around AWS Lambda's `SQSEvent` class. Instead of manually parsing JSON from `record.Body` strings, you get strongly-typed access to deserialized message payloads via `record.BodyContent`. + +**When to use**: + +- ✅ Processing SQS messages with structured JSON payloads +- ✅ You want compile-time type safety for message handling +- ✅ You need IDE IntelliSense support for message properties +- ✅ Implementing SNS-to-SQS subscription patterns +- ✅ You want to avoid manual JSON parsing boilerplate + +--- + +## Installation + +```bash +dotnet add package AwsLambda.Host.Envelopes.Sqs +``` + +Requires `AwsLambda.Host` to be installed. + +--- + +## Classes Provided + +| Class | Base Class | Use Case | +|-------|-----------|----------| +| **`SqsEnvelope`** | `SQSEvent` | SQS messages with deserialized JSON message bodies | +| **`SqsSnsEnvelope`** | `SqsEnvelopeBase` | SQS messages containing SNS notifications (SNS-to-SQS pattern) | + +Both classes extend `SqsEnvelopeBase`, which provides the core envelope functionality. + +--- + +## Quick Start + +Define your message type and handler: + +```csharp title="Program.cs" linenums="1" +using Amazon.Lambda.SQSEvents; +using AwsLambda.Host.Builder; +using AwsLambda.Host.Envelopes.Sqs; +using Microsoft.Extensions.Logging; + +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +// SqsEnvelope provides access to SQS event and deserialized Message payloads +lambda.MapHandler( + ([Event] SqsEnvelope envelope, ILogger logger) => + { + // Return SQSBatchResponse to handle partial failures + var batchResponse = new SQSBatchResponse(); + + foreach (var record in envelope.Records) + { + // Add failure if message body couldn't be deserialized + if (record.BodyContent is null) + { + batchResponse.BatchItemFailures.Add( + new SQSBatchResponse.BatchItemFailure + { + ItemIdentifier = record.MessageId + } + ); + continue; + } + + logger.LogInformation( + "Processing message: {Name}", + record.BodyContent.Name + ); + } + + return batchResponse; + } +); + +await lambda.RunAsync(); + +// Your message payload - deserialized from SQS message body +internal record Message(string Name); +``` + +--- + +## Request Envelope (`SqsEnvelope`) + +### Basic Usage + +The `SqsEnvelope` class extends `SQSEvent` and provides type-safe access to deserialized message payloads: + +```csharp title="Program.cs" linenums="1" +lambda.MapHandler( + ([Event] SqsEnvelope envelope, IOrderProcessor processor) => + { + foreach (var record in envelope.Records) + { + if (record.BodyContent is null) + { + Console.WriteLine($"Failed to deserialize message {record.MessageId}"); + continue; + } + + // Access strongly-typed properties + processor.ProcessOrder( + record.BodyContent.OrderId, + record.BodyContent.CustomerId, + record.BodyContent.TotalAmount + ); + + // Access original SQS message properties + Console.WriteLine($"Message ID: {record.MessageId}"); + Console.WriteLine($"Receipt Handle: {record.ReceiptHandle}"); + Console.WriteLine($"Sent Timestamp: {record.Attributes["SentTimestamp"]}"); + } + + return new SQSBatchResponse(); + } +); + +internal record OrderMessage( + string OrderId, + string CustomerId, + decimal TotalAmount, + DateTime OrderDate +); +``` + +### Accessing Messages + +Each message in `envelope.Records` has: + +- **`BodyContent`** (type `T?`) - Deserialized message payload (null if deserialization failed) +- **All `SQSMessage` properties** - `MessageId`, `ReceiptHandle`, `Body`, `Attributes`, `MessageAttributes`, etc. + +```csharp title="Accessing message properties" +foreach (var record in envelope.Records) +{ + // Deserialized payload + var payload = record.BodyContent; // Type: OrderMessage? + + // Original SQS message properties + var messageId = record.MessageId; + var receiptHandle = record.ReceiptHandle; + var rawBody = record.Body; // Original JSON string + var attributes = record.Attributes; + var messageAttributes = record.MessageAttributes; +} +``` + +### Batch Response Handling + +SQS supports partial batch failures. Return `SQSBatchResponse` with failed message IDs: + +```csharp title="Partial batch failure handling" linenums="1" +lambda.MapHandler( + ([Event] SqsEnvelope envelope) => + { + var batchResponse = new SQSBatchResponse(); + + foreach (var record in envelope.Records) + { + try + { + if (record.BodyContent is null) + throw new InvalidOperationException("Deserialization failed"); + + // Process message + ProcessMessage(record.BodyContent); + } + catch (Exception ex) + { + Console.WriteLine($"Error processing {record.MessageId}: {ex.Message}"); + + // Mark message as failed - will be retried + batchResponse.BatchItemFailures.Add( + new SQSBatchResponse.BatchItemFailure + { + ItemIdentifier = record.MessageId + } + ); + } + } + + return batchResponse; + } +); +``` + +!!! tip "Partial Batch Failure Best Practices" + - Always return `SQSBatchResponse` even if all messages succeed (empty failures list) + - Add failed message IDs to `BatchItemFailures` - SQS will retry only those messages + - Failed messages are made visible again and will be redelivered + - Successful messages are deleted from the queue + +--- + +## SNS-to-SQS Pattern (`SqsSnsEnvelope`) + +### When to Use + +Use `SqsSnsEnvelope` when: + +- SNS topic delivers messages to an SQS queue (SNS-to-SQS subscription) +- Your Lambda function processes messages from that SQS queue +- You need to access both SNS message metadata AND the typed payload + +### Two-Stage Deserialization + +`SqsSnsEnvelope` performs two-stage deserialization: + +1. **Stage 1**: SQS message body → SNS message envelope +2. **Stage 2**: SNS message content → Your payload type `T` + +```csharp title="SNS-to-SQS pattern" linenums="1" +using AwsLambda.Host.Envelopes.Sns; + +lambda.MapHandler( + ([Event] SqsSnsEnvelope envelope, ILogger logger) => + { + var batchResponse = new SQSBatchResponse(); + + foreach (var record in envelope.Records) + { + if (record.BodyContent is null) + { + batchResponse.BatchItemFailures.Add( + new SQSBatchResponse.BatchItemFailure + { + ItemIdentifier = record.MessageId + } + ); + continue; + } + + // Access SNS message envelope + var snsMessage = record.BodyContent; + + // Access deserialized SNS message content + var notification = snsMessage.MessageContent; + + logger.LogInformation( + "SNS Topic: {Topic}, Subject: {Subject}, Message: {Message}", + snsMessage.TopicArn, + snsMessage.Subject, + notification?.Message + ); + } + + return batchResponse; + } +); + +internal record Notification(string Message, DateTime Timestamp); +``` + +### SNS Message Properties + +The `BodyContent` provides access to SNS message envelope: + +```csharp +foreach (var record in envelope.Records) +{ + var snsEnvelope = record.BodyContent; // Type: SnsMessageEnvelope? + + // SNS message metadata + var topicArn = snsEnvelope.TopicArn; + var subject = snsEnvelope.Subject; + var messageId = snsEnvelope.MessageId; + var timestamp = snsEnvelope.Timestamp; + var messageAttributes = snsEnvelope.MessageAttributes; + + // Deserialized payload from SNS message + var payload = snsEnvelope.MessageContent; // Type: Notification? +} +``` + +--- + +## Configuration + +### Envelope Options + +Configure JSON serialization options for envelope payloads: + +```csharp title="Program.cs" linenums="1" +using System.Text.Json; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.ConfigureEnvelopeOptions(options => +{ + // Use snake_case for JSON property names + options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + + // Case-insensitive property matching + options.JsonOptions.PropertyNameCaseInsensitive = true; + + // Allow trailing commas in JSON + options.JsonOptions.AllowTrailingCommas = true; +}); + +var lambda = builder.Build(); +``` + +### Common Configuration Patterns + +```csharp title="Configuration examples" linenums="1" +builder.Services.ConfigureEnvelopeOptions(options => +{ + // Camel case properties + options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + // Write indented JSON for debugging + options.JsonOptions.WriteIndented = true; + + // Handle missing properties gracefully + options.JsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + + // Custom number handling + options.JsonOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; +}); +``` + +--- + +## Native AOT Support + +### JsonSerializerContext Registration + +For Native AOT compilation, register envelope and payload types in your `JsonSerializerContext`: + +```csharp title="Program.cs" linenums="1" +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(SqsEnvelope))] +[JsonSerializable(typeof(Message))] +internal partial class SerializerContext : JsonSerializerContext; +``` + +### Dual Context Registration + +**Critical**: Register the context in **both** the Lambda serializer AND envelope options: + +```csharp title="Program.cs" linenums="1" +var builder = LambdaApplication.CreateBuilder(); + +// 1. Register for Lambda event deserialization +builder.Services.AddLambdaSerializerWithContext(); + +// 2. Register for envelope payload deserialization +builder.Services.ConfigureEnvelopeOptions(options => +{ + options.JsonOptions.TypeInfoResolver = SerializerContext.Default; +}); + +var lambda = builder.Build(); +``` + +!!! warning "Why Two Registrations?" + The context must be registered twice because deserialization happens in two stages: + + 1. **Lambda serializer** deserializes the raw `SQSEvent` from Lambda runtime + 2. **Envelope options** deserialize the message body content into your payload type `T` + + Both stages need access to the `JsonSerializerContext` for AOT compilation. + +### SNS-to-SQS AOT Configuration + +For `SqsSnsEnvelope`, you must register additional types with unique `TypeInfoPropertyName`: + +```csharp title="SNS-to-SQS AOT context" linenums="1" +using Amazon.Lambda.SNSEvents; +using Amazon.Lambda.SQSEvents; + +[JsonSerializable(typeof(SqsSnsEnvelope))] +[JsonSerializable(typeof(SnsEnvelope.SnsMessageEnvelope))] +[JsonSerializable(typeof(SNSEvent.MessageAttribute), TypeInfoPropertyName = "SnsMessageAttribute")] +[JsonSerializable(typeof(SQSEvent.MessageAttribute), TypeInfoPropertyName = "SqsMessageAttribute")] +[JsonSerializable(typeof(Notification))] +internal partial class SerializerContext : JsonSerializerContext; +``` + +!!! danger "MessageAttribute Naming Collision" + Both `SNSEvent.MessageAttribute` and `SQSEvent.MessageAttribute` have the same type name. Without unique `TypeInfoPropertyName` values, AOT compilation will fail with a naming conflict. Always specify unique names for these types. + +--- + +## Custom Envelopes + +### XML Serialization Example + +Extend `SqsEnvelopeBase` to support custom serialization formats: + +```csharp title="SqsXmlEnvelope.cs" linenums="1" +using System.Xml; +using System.Xml.Serialization; +using AwsLambda.Host.Abstractions.Options; +using AwsLambda.Host.Envelopes.Sqs; + +public sealed class SqsXmlEnvelope : SqsEnvelopeBase +{ + private static readonly XmlSerializer Serializer = new(typeof(T)); + + public override void ExtractPayload(EnvelopeOptions options) + { + foreach (var record in Records) + { + using var stringReader = new StringReader(record.Body); + using var xmlReader = XmlReader.Create(stringReader, options.XmlReaderSettings); + record.BodyContent = (T?)Serializer.Deserialize(xmlReader); + } + } +} +``` + +**Usage**: + +```csharp title="Using XML envelope" +lambda.MapHandler( + ([Event] SqsXmlEnvelope envelope) => + { + foreach (var record in envelope.Records) + { + Console.WriteLine($"Deserialized XML: {record.BodyContent?.Data}"); + } + + return new SQSBatchResponse(); + } +); + +public class XmlMessage +{ + public string? Data { get; set; } +} +``` + +### Custom Serialization Formats + +You can implement any serialization format by overriding `ExtractPayload`: + +```csharp title="Protocol Buffers example (conceptual)" +public sealed class SqsProtobufEnvelope : SqsEnvelopeBase where T : IMessage, new() +{ + public override void ExtractPayload(EnvelopeOptions options) + { + var parser = new MessageParser(() => new T()); + + foreach (var record in Records) + { + var bytes = Convert.FromBase64String(record.Body); + record.BodyContent = parser.ParseFrom(bytes); + } + } +} +``` + +--- + +## Common Patterns + +### Validation + +Validate deserialized payloads before processing: + +```csharp title="Payload validation" linenums="1" +lambda.MapHandler( + ([Event] SqsEnvelope envelope, IValidator validator) => + { + var batchResponse = new SQSBatchResponse(); + + foreach (var record in envelope.Records) + { + if (record.BodyContent is null) + { + Console.WriteLine($"Deserialization failed for {record.MessageId}"); + batchResponse.BatchItemFailures.Add( + new SQSBatchResponse.BatchItemFailure + { + ItemIdentifier = record.MessageId + } + ); + continue; + } + + var validationResult = validator.Validate(record.BodyContent); + if (!validationResult.IsValid) + { + Console.WriteLine($"Validation failed: {string.Join(", ", validationResult.Errors)}"); + batchResponse.BatchItemFailures.Add( + new SQSBatchResponse.BatchItemFailure + { + ItemIdentifier = record.MessageId + } + ); + continue; + } + + // Process valid message + ProcessOrder(record.BodyContent); + } + + return batchResponse; + } +); +``` + +### Error Handling with Logging + +```csharp title="Error handling pattern" linenums="1" +lambda.MapHandler( + ([Event] SqsEnvelope envelope, ILogger logger) => + { + var batchResponse = new SQSBatchResponse(); + + foreach (var record in envelope.Records) + { + try + { + if (record.BodyContent is null) + { + logger.LogError( + "Failed to deserialize message {MessageId}. Body: {Body}", + record.MessageId, + record.Body + ); + throw new InvalidOperationException("Deserialization failed"); + } + + // Process message + ProcessMessage(record.BodyContent); + + logger.LogInformation( + "Successfully processed message {MessageId}", + record.MessageId + ); + } + catch (Exception ex) + { + logger.LogError( + ex, + "Error processing message {MessageId}", + record.MessageId + ); + + batchResponse.BatchItemFailures.Add( + new SQSBatchResponse.BatchItemFailure + { + ItemIdentifier = record.MessageId + } + ); + } + } + + return batchResponse; + } +); +``` + +### Accessing SQS Message Attributes + +```csharp title="Reading message attributes" linenums="1" +foreach (var record in envelope.Records) +{ + // Standard SQS attributes + var sentTimestamp = record.Attributes.GetValueOrDefault("SentTimestamp"); + var approximateReceiveCount = record.Attributes.GetValueOrDefault("ApproximateReceiveCount"); + + // Custom message attributes + if (record.MessageAttributes.TryGetValue("Priority", out var priorityAttr)) + { + var priority = priorityAttr.StringValue; + Console.WriteLine($"Message priority: {priority}"); + } + + // Process with context + if (int.TryParse(approximateReceiveCount, out var receiveCount)) + { + if (receiveCount > 3) + { + Console.WriteLine($"Message {record.MessageId} has been retried {receiveCount} times"); + // Consider moving to DLQ + } + } +} +``` + +--- + +## Best Practices + +### ✅ Do + +- **Always return `SQSBatchResponse`** even if all messages succeed +- **Check for null `BodyContent`** before processing (deserialization can fail) +- **Use partial batch failures** to retry only failed messages +- **Register types for AOT** when using Native AOT compilation +- **Register context in both places** for AOT (Lambda serializer + envelope options) +- **Log deserialization failures** with message ID and body for debugging +- **Use message attributes** for filtering and routing +- **Implement idempotency** - SQS may deliver messages more than once + +### ❌ Don't + +- **Don't throw unhandled exceptions** - use `SQSBatchResponse.BatchItemFailures` instead +- **Don't process without null checks** - `BodyContent` can be null if deserialization fails +- **Don't retry all messages** when only some fail - use partial batch failures +- **Don't forget dual context registration** for AOT (will cause runtime errors) +- **Don't ignore `ApproximateReceiveCount`** - monitor for poison pill messages +- **Don't skip DLQ configuration** - messages failing repeatedly should go to DLQ + +--- + +## Troubleshooting + +### "`BodyContent` is always null" + +**Cause**: JSON deserialization is failing silently. + +**Solutions**: + +1. Check your message payload structure matches the type definition: + ```csharp + // Ensure property names match (or use [JsonPropertyName]) + internal record Message( + [property: JsonPropertyName("message_name")] string MessageName + ); + ``` + +2. Enable case-insensitive matching: + ```csharp + builder.Services.ConfigureEnvelopeOptions(options => + { + options.JsonOptions.PropertyNameCaseInsensitive = true; + }); + ``` + +3. Log raw message body to inspect structure: + ```csharp + if (record.BodyContent is null) + { + Console.WriteLine($"Failed to deserialize: {record.Body}"); + } + ``` + +### "AOT compilation fails with naming conflict" + +**Error**: `error CS0102: The type 'JsonContext' already contains a definition for 'MessageAttribute'` + +**Cause**: Both `SNSEvent.MessageAttribute` and `SQSEvent.MessageAttribute` registered without unique names. + +**Solution**: Use `TypeInfoPropertyName` for `SqsSnsEnvelope`: +```csharp +[JsonSerializable(typeof(SNSEvent.MessageAttribute), TypeInfoPropertyName = "SnsMessageAttribute")] +[JsonSerializable(typeof(SQSEvent.MessageAttribute), TypeInfoPropertyName = "SqsMessageAttribute")] +``` + +### "Messages retrying indefinitely" + +**Cause**: Not using `SQSBatchResponse` to mark failures, or throwing exceptions instead. + +**Solution**: Always return `SQSBatchResponse` with failed message IDs: +```csharp +var batchResponse = new SQSBatchResponse(); + +try +{ + ProcessMessage(record.BodyContent); +} +catch +{ + batchResponse.BatchItemFailures.Add( + new SQSBatchResponse.BatchItemFailure { ItemIdentifier = record.MessageId } + ); +} + +return batchResponse; // Don't throw! +``` + +--- + +## Key Takeaways + +1. **Type-safe payloads** - `SqsEnvelope` eliminates manual JSON parsing +2. **Two classes** - `SqsEnvelope` for SQS, `SqsSnsEnvelope` for SNS-to-SQS +3. **Partial batch failures** - Return `SQSBatchResponse` with failed message IDs +4. **Null checks required** - `BodyContent` can be null if deserialization fails +5. **AOT needs two registrations** - Lambda serializer AND envelope options +6. **Custom formats supported** - Extend `SqsEnvelopeBase` for XML, Protobuf, etc. +7. **SNS-to-SQS requires special handling** - Unique `TypeInfoPropertyName` for MessageAttribute types + +--- + +## Next Steps + +- **[Envelope Pattern Overview](index.md)** - Learn how all envelopes work +- **[SNS Envelope](sns.md)** - Process SNS notifications with type safety +- **[API Gateway Envelope](api-gateway.md)** - Handle API Gateway requests/responses +- **[Configuration Guide](../../guides/configuration.md)** - Configure envelope serialization options +- **[Error Handling Guide](../../guides/error-handling.md)** - Error handling patterns for Lambda +- **[Deployment Guide](../../guides/deployment.md)** - Deploy SQS-triggered Lambda functions diff --git a/docs/features/index.md b/docs/features/index.md new file mode 100644 index 00000000..a705448c --- /dev/null +++ b/docs/features/index.md @@ -0,0 +1,241 @@ +# Features + +The aws-lambda-host framework provides a rich ecosystem of features and extension packages that enhance AWS Lambda development beyond the core framework capabilities. + +--- + +## Core Framework vs Features + +The framework is organized into two main categories: + +### Core Framework (`AwsLambda.Host`) + +The core framework provides the foundational hosting capabilities: + +- **.NET Hosting Patterns** - Middleware, builder pattern, and dependency injection (similar to ASP.NET Core) +- **Async-First Design** - Native async/await support with Lambda timeout and cancellation handling +- **Source Generators & Interceptors** - Compile-time code generation for optimal performance +- **Flexible Handler Registration** - Simple, declarative API with the `[Event]` attribute +- **Lifecycle Management** - OnInit, Invocation, and OnShutdown phases + +Learn more in the **[Guides](../guides/index.md)** section. + +### Features & Extensions + +Features extend the core framework with specialized capabilities for specific use cases: + +- **Envelopes** - Type-safe event source integration packages +- **Observability** - OpenTelemetry integration for distributed tracing and metrics + +--- + +## Feature Categories + +### 1. Envelope Pattern + +**What are Envelopes?** + +Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) and add a `BodyContent` property that provides type-safe access to deserialized message payloads. Instead of manually parsing JSON strings from event bodies, you get strongly-typed objects with full IDE support and compile-time type checking. + +**Key Benefits**: + +- **Type Safety** - Generic type parameter `` ensures compile-time type checking +- **Extensibility** - Abstract base classes allow custom serialization formats (JSON, XML, etc.) +- **Zero Overhead** - Envelopes extend official AWS event types, adding no runtime cost +- **AOT Ready** - Support for Native AOT compilation via `JsonSerializerContext` registration +- **Familiar API** - Works seamlessly with existing AWS Lambda event patterns + +**Supported Event Sources**: + +| Event Source | Package | Use Case | +|--------------------------------------|--------------------------------------------------------------------------|---------------------------------------------------------| +| **SQS** | [AwsLambda.Host.Envelopes.Sqs](envelopes/sqs.md) | Queue message processing with type-safe payloads | +| **SNS** | [AwsLambda.Host.Envelopes.Sns](envelopes/sns.md) | Pub/sub notifications with typed messages | +| **API Gateway** | [AwsLambda.Host.Envelopes.ApiGateway](envelopes/api-gateway.md) | REST/HTTP/WebSocket APIs with request/response envelopes| +| **Kinesis Data Streams** | [AwsLambda.Host.Envelopes.Kinesis](envelopes/kinesis.md) | Stream processing with typed records | +| **Kinesis Data Firehose** | [AwsLambda.Host.Envelopes.KinesisFirehose](envelopes/kinesis-firehose.md)| Data transformation with typed payloads | +| **Kafka (MSK or self-managed)** | [AwsLambda.Host.Envelopes.Kafka](envelopes/kafka.md) | Event streaming with typed messages | +| **CloudWatch Logs** | [AwsLambda.Host.Envelopes.CloudWatchLogs](envelopes/cloudwatch-logs.md) | Log processing with typed log events | +| **Application Load Balancer** | [AwsLambda.Host.Envelopes.Alb](envelopes/alb.md) | ALB target integration with request/response envelopes | + +!!! tip "Learn More About Envelopes" + - **[Envelope Pattern Overview](envelopes/index.md)** - Detailed explanation of how envelopes work + - **[Creating Custom Envelopes](envelopes/custom-envelopes.md)** - Build your own envelope implementations + +### 2. Observability (OpenTelemetry) + +**What is OpenTelemetry Integration?** + +The `AwsLambda.Host.OpenTelemetry` package provides comprehensive observability integration for distributed tracing and metrics collection in AWS Lambda functions. + +**Capabilities**: + +- **Distributed Tracing** - Automatic span creation and context propagation for Lambda invocations +- **Metrics Collection** - Performance and business metrics exportable to standard observability backends +- **AWS Lambda Instrumentation** - Lambda-specific insights including cold starts, warm invocations, and error tracking +- **Lifecycle Integration** - Seamless integration with OnInit, Invocation, and OnShutdown phases +- **Vendor-Neutral** - Built on the OpenTelemetry SDK for compatibility with any observability backend + +**Supported Exporters**: + +- OTLP (OpenTelemetry Protocol) +- Jaeger +- AWS X-Ray +- Datadog +- New Relic +- CloudWatch +- And more... + +!!! info "Learn More About OpenTelemetry" + - **[OpenTelemetry Integration Guide](opentelemetry.md)** - Complete setup and configuration + +--- + +## Design Philosophy + +All features in the aws-lambda-host ecosystem follow these principles: + +1. **Zero Runtime Overhead** - Features extend existing types rather than wrapping them +2. **AOT Compatibility** - Full support for Native AOT compilation +3. **Type Safety** - Compile-time type checking wherever possible +4. **Extensibility** - Abstract base classes and interfaces for custom implementations +5. **Familiar .NET Patterns** - Leverage existing .NET idioms and practices + +--- + +## Quick Start + +### Using Envelopes + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.Builder; +using AwsLambda.Host.Envelopes.Sqs; + +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +// Type-safe SQS message processing +lambda.MapHandler( + ([Event] SqsEnvelope envelope, ILogger logger) => + { + foreach (var record in envelope.Records) + { + if (record.BodyContent is null) + continue; + + logger.LogInformation( + "Processing order: {OrderId}", + record.BodyContent.OrderId + ); + } + + return new SQSBatchResponse(); + } +); + +await lambda.RunAsync(); + +internal record OrderMessage(string OrderId, decimal Amount); +``` + +### Using OpenTelemetry + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.Builder; +using AwsLambda.Host.OpenTelemetry; + +var builder = LambdaApplication.CreateBuilder(); + +// Add OpenTelemetry tracing and metrics +builder.Services.AddLambdaOpenTelemetry(); + +var lambda = builder.Build(); + +lambda.MapHandler(([Event] Request request) => +{ + // Automatic span creation for this invocation + return new Response($"Hello {request.Name}!"); +}); + +await lambda.RunAsync(); + +internal record Request(string Name); +internal record Response(string Message); +``` + +--- + +## Choosing the Right Feature + +### When to Use Envelopes + +Use envelope packages when: + +- ✅ You need type-safe access to message payloads from AWS event sources +- ✅ You want compile-time type checking for event data +- ✅ You're tired of manually parsing JSON from event bodies +- ✅ You need custom serialization formats (XML, Protobuf, etc.) +- ✅ You want IDE IntelliSense support for message structures + +### When to Use OpenTelemetry + +Use the OpenTelemetry package when: + +- ✅ You need distributed tracing across microservices +- ✅ You want to monitor Lambda performance and cold starts +- ✅ You need to export metrics to observability platforms (Datadog, New Relic, etc.) +- ✅ You want automatic instrumentation for Lambda invocations +- ✅ You're building complex serverless architectures + +--- + +## Installation + +### Core Framework (Required) + +All features require the core framework: + +```bash +dotnet add package AwsLambda.Host +``` + +### Envelope Packages (Optional) + +Install only the envelope packages you need: + +```bash +# SQS envelope +dotnet add package AwsLambda.Host.Envelopes.Sqs + +# API Gateway envelope +dotnet add package AwsLambda.Host.Envelopes.ApiGateway + +# Other envelopes... +``` + +### OpenTelemetry Package (Optional) + +```bash +dotnet add package AwsLambda.Host.OpenTelemetry +``` + +--- + +## Next Steps + +Explore specific features: + +- **[Envelope Pattern](envelopes/index.md)** - Learn how envelopes work and browse supported event sources +- **[OpenTelemetry Integration](opentelemetry.md)** - Set up distributed tracing and metrics +- **[Guides](../guides/index.md)** - Learn about core framework capabilities +- **[Examples](../examples/index.md)** - See complete examples using features + +--- + +## Key Takeaways + +1. **Features extend the core** - All features build on top of `AwsLambda.Host` +2. **Zero overhead design** - Envelopes extend official AWS types with no runtime cost +3. **Pick what you need** - Install only the envelope packages relevant to your use case +4. **AOT compatible** - All features support Native AOT compilation +5. **Type-safe by default** - Envelopes provide compile-time type checking for event payloads diff --git a/mkdocs.yml b/mkdocs.yml index f179c8d4..6625fbbd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -98,6 +98,20 @@ nav: - Error Handling: guides/error-handling.md - Testing: guides/testing.md - Deployment: guides/deployment.md + - Features: + - features/index.md + - Envelopes: + - features/envelopes/index.md + - SQS: features/envelopes/sqs.md + - SNS: features/envelopes/sns.md + - API Gateway: features/envelopes/api-gateway.md + - Kinesis: features/envelopes/kinesis.md + - Kinesis Firehose: features/envelopes/kinesis-firehose.md + - Kafka: features/envelopes/kafka.md + - CloudWatch Logs: features/envelopes/cloudwatch-logs.md + - ALB: features/envelopes/alb.md + - Custom Envelopes: features/envelopes/custom-envelopes.md + - OpenTelemetry: features/opentelemetry.md plugins: - search From 7739c3ccd84da454c33a2eb08180890513ea490f Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 14:15:11 -0500 Subject: [PATCH 18/65] chore(docs): remove SQS envelope documentation and add envelope overview - Removed `docs/features/envelopes/sqs.md` as part of restructuring envelope content. - Consolidated envelope documentation into `docs/features/envelopes/index.md` for a centralized overview. - Enhanced `index.md` with details on supported event sources, benefits, and configuration methods. - Updated examples to showcase the generic envelope pattern instead of SQS-specific content. --- docs/features/envelopes/index.md | 575 +++++++++++++++++++++++++ docs/features/envelopes/sqs.md | 709 ------------------------------- docs/features/open_telemetry.md | 79 ++++ mkdocs.yml | 2 +- 4 files changed, 655 insertions(+), 710 deletions(-) create mode 100644 docs/features/envelopes/index.md delete mode 100644 docs/features/envelopes/sqs.md create mode 100644 docs/features/open_telemetry.md diff --git a/docs/features/envelopes/index.md b/docs/features/envelopes/index.md new file mode 100644 index 00000000..4fee0a5b --- /dev/null +++ b/docs/features/envelopes/index.md @@ -0,0 +1,575 @@ +# Envelope + +**What are Envelopes?** + +Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) and add a `BodyContent` property that provides type-safe access to deserialized message payloads. Instead of manually parsing JSON strings from event bodies, you get strongly-typed objects with full IDE support and compile-time type checking. + +**Key Benefits**: + +- **Type Safety** - Generic type parameter `` ensures compile-time type checking +- **Extensibility** - Abstract base classes allow custom serialization formats (JSON, XML, etc.) +- **Zero Overhead** - Envelopes extend official AWS event types, adding no runtime cost +- **AOT Ready** - Support for Native AOT compilation via `JsonSerializerContext` registration +- **Familiar API** - Works seamlessly with existing AWS Lambda event patterns + +**Supported Event Sources**: + +| Event Source | Package | NuGet | Use Case | +|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| +| **SQS** | [AwsLambda.Host.Envelopes.Sqs](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sqs) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs) | Queue message processing with type-safe payloads | +| **SNS** | [AwsLambda.Host.Envelopes.Sns](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sns) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns) | Pub/sub notifications with typed messages | +| **API Gateway** | [AwsLambda.Host.Envelopes.ApiGateway](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.ApiGateway) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway) | REST/HTTP/WebSocket APIs with request/response envelopes | +| **Kinesis Data Streams** | [AwsLambda.Host.Envelopes.Kinesis](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Kinesis) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis) | Stream processing with typed records | +| **Kinesis Data Firehose** | [AwsLambda.Host.Envelopes.KinesisFirehose](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.KinesisFirehose) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose) | Data transformation with typed payloads | +| **Kafka (MSK or self-managed)** | [AwsLambda.Host.Envelopes.Kafka](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Kafka) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka) | Event streaming with typed messages | +| **CloudWatch Logs** | [AwsLambda.Host.Envelopes.CloudWatchLogs](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.CloudWatchLogs) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs) | Log processing with typed log events | +| **Application Load Balancer** | [AwsLambda.Host.Envelopes.Alb](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Alb) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb) | ALB target integration with request/response envelopes | + +!!! tip "Learn More About Envelopes" + For detailed implementation examples and API documentation, see the individual package README files on GitHub (linked in the table above). + +## Quick Start + +### Using Envelopes + +This example demonstrates the envelope pattern using SQS, but the same pattern applies to all envelope types (SNS, API Gateway, Kinesis, etc.) - simply swap `SqsEnvelope` for the appropriate envelope type. + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.Builder; +using AwsLambda.Host.Envelopes.Sqs; + +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +// Type-safe SQS message processing +lambda.MapHandler( + ([Event] SqsEnvelope envelope, ILogger logger) => + { + foreach (var record in envelope.Records) + { + if (record.BodyContent is null) + continue; + + logger.LogInformation( + "Processing order: {OrderId}", + record.BodyContent.OrderId + ); + } + + return new SQSBatchResponse(); + } +); + +await lambda.RunAsync(); + +internal record OrderMessage(string OrderId, decimal Amount); +``` + +--- + +## Custom Serialization + +Envelopes support custom serialization formats beyond JSON. By extending envelope base classes, you can implement XML, Protocol Buffers, or any other serialization format. + +**Example use cases:** + +- Legacy systems using XML message formats +- High-performance scenarios requiring Protocol Buffers +- Custom binary formats for specialized domains +- Integration with third-party systems using non-JSON formats + +For implementation examples, see the [SQS Envelope README](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sqs#custom-envelopes) which demonstrates XML serialization. + +--- + +## Configuration + +Envelope configuration is managed through the `EnvelopeOptions` class, which controls how envelope payloads are serialized and deserialized. Configuration applies globally to all envelope types in your application. + +### Quick Start + +The most common configuration scenario is customizing JSON serialization: + +```csharp title="Program.cs" linenums="1" +using System.Text.Json; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.ConfigureEnvelopeOptions(options => +{ + // Use snake_case for JSON property names + options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + + // Case-insensitive property matching + options.JsonOptions.PropertyNameCaseInsensitive = true; + + // Allow trailing commas in JSON + options.JsonOptions.AllowTrailingCommas = true; +}); + +var lambda = builder.Build(); +``` + +--- + +### Configuration Properties + +`EnvelopeOptions` provides five configuration properties for different serialization scenarios: + +| Property | Type | Purpose | Default | When to Use | +|----------|------|---------|---------|-------------| +| **JsonOptions** | `JsonSerializerOptions` | JSON serialization/deserialization for envelope payloads | Empty options | Most common - configure naming policies, converters, AOT support | +| **LambdaDefaultJsonOptions** | `JsonSerializerOptions` | AWS Lambda-specific JSON settings for complex envelope payloads | AWS defaults¹ | Automatic - rarely needs manual configuration | +| **XmlReaderSettings** | `XmlReaderSettings` | XML deserialization settings | Default settings | Custom envelopes using XML serialization | +| **XmlWriterSettings** | `XmlWriterSettings` | XML serialization settings | Default settings | Custom envelopes using XML serialization | +| **Items** | `Dictionary` | Custom extension data | Empty dictionary | Advanced - store custom context for envelope processing | + +¹ *AWS defaults include: case-insensitive property matching, AWS naming policy (PascalCase), DateTime/MemoryStream/ByteArray converters* + +!!! info "LambdaDefaultJsonOptions Behavior" + `LambdaDefaultJsonOptions` is automatically configured with AWS Lambda-compatible settings and is used internally for complex envelope types like SNS-to-SQS and CloudWatch Logs. During post-configuration, the framework automatically copies `JsonOptions.TypeInfoResolver` to `LambdaDefaultJsonOptions` to ensure AOT compatibility works correctly across all envelope types. + +--- + +### JSON Configuration + +Configure JSON serialization for envelope payloads: + +```csharp title="Common JSON patterns" linenums="1" +using System.Text.Json; +using System.Text.Json.Serialization; + +builder.Services.ConfigureEnvelopeOptions(options => +{ + // Naming policies + options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + // Write indented JSON for debugging + options.JsonOptions.WriteIndented = true; + + // Handle null values + options.JsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + + // Number handling + options.JsonOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; + + // Custom converters + options.JsonOptions.Converters.Add(new MyCustomConverter()); +}); +``` + +#### Native AOT Support + +For Native AOT compilation, register a `JsonSerializerContext`: + +```csharp title="AOT-compatible configuration" linenums="1" +using System.Text.Json.Serialization; + +// Define your JSON source generation context +[JsonSerializable(typeof(OrderMessage))] +[JsonSerializable(typeof(SqsEnvelope))] +internal partial class MyJsonContext : JsonSerializerContext { } + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.ConfigureEnvelopeOptions(options => +{ + // Register source generation context for AOT + options.JsonOptions.TypeInfoResolver = MyJsonContext.Default; +}); + +var lambda = builder.Build(); +``` + +!!! tip "AOT Compatibility" + The framework automatically copies `TypeInfoResolver` from `JsonOptions` to `LambdaDefaultJsonOptions`, ensuring all envelope types work correctly with Native AOT compilation. + +--- + +### XML Configuration + +For custom envelopes using XML serialization (see [Custom Serialization](#custom-serialization)): + +```csharp title="XML configuration" linenums="1" +using System.Xml; + +builder.Services.ConfigureEnvelopeOptions(options => +{ + // XML reader settings (deserialization) + options.XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit; + options.XmlReaderSettings.IgnoreWhitespace = true; + options.XmlReaderSettings.IgnoreComments = true; + + // XML writer settings (serialization) + options.XmlWriterSettings.Indent = true; + options.XmlWriterSettings.IndentChars = " "; + options.XmlWriterSettings.OmitXmlDeclaration = false; +}); +``` + +--- + +### Advanced: Custom Extension Data + +The `Items` dictionary allows storing custom context or configuration data for envelope processing: + +```csharp title="Using Items dictionary" linenums="1" +builder.Services.ConfigureEnvelopeOptions(options => +{ + // Store custom context for envelope implementations + options.Items["SchemaVersion"] = "2.0"; + options.Items["ValidationEnabled"] = true; + options.Items["CustomProcessor"] = new MyCustomProcessor(); +}); +``` + +**Example: Accessing Items in a custom envelope** + +```csharp title="CustomEnvelope.cs" linenums="1" +public class CustomEnvelope : IRequestEnvelope +{ + public void ExtractPayload(EnvelopeOptions options) + { + // Access custom configuration + if (options.Items.TryGetValue("ValidationEnabled", out var enabled) + && enabled is true) + { + ValidatePayload(); + } + + // Deserialize payload... + } +} +``` + +--- + +## Creating Custom Envelopes + +Custom envelopes allow you to define your own event types with nested payloads that are automatically extracted and packed by the framework. This is useful when you need custom event structures beyond AWS's standard event types. + +### When to Use Custom Envelopes + +Create custom envelopes when: + +- ✅ You're defining your own event structure (not using AWS events like SQS, SNS, etc.) +- ✅ Your event contains a serialized payload that needs deserialization +- ✅ You want automatic payload extraction/packing by the framework +- ✅ You need type-safe access to nested event data + +!!! note "Custom Serialization vs Custom Envelopes" + **Custom envelopes** = Your own event types implementing `IRequestEnvelope`/`IResponseEnvelope` + + **Custom serialization** = Using XML, Protobuf, etc. with existing AWS events (see the [SQS Envelope README](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sqs#custom-envelopes) for examples) + +### Core Interfaces + +#### IRequestEnvelope + +Implement this interface for incoming events that contain nested payloads: + +```csharp +public interface IRequestEnvelope +{ + void ExtractPayload(EnvelopeOptions options); +} +``` + +The framework automatically calls `ExtractPayload()` **before** your handler executes, allowing you to deserialize nested data. + +#### IResponseEnvelope + +Implement this interface for outgoing responses that need serialization: + +```csharp +public interface IResponseEnvelope +{ + void PackPayload(EnvelopeOptions options); +} +``` + +The framework automatically calls `PackPayload()` **after** your handler executes, serializing your response data. + +--- + +### Example 1: Simple Request Envelope + +A custom event with metadata and a nested JSON payload: + +```csharp title="CustomRequestEvent.cs" linenums="1" +using System.Text.Json; +using System.Text.Json.Serialization; +using AwsLambda.Host.Abstractions.Options; +using AwsLambda.Host.Envelopes; + +public class CustomRequestEvent : IRequestEnvelope +{ + [JsonPropertyName("metadata")] + public required EventMetadata Metadata { get; set; } + + [JsonPropertyName("payload")] + public required string Payload { get; set; } // Serialized JSON string + + [JsonIgnore] + public MyData? PayloadContent { get; set; } // Deserialized object + + public void ExtractPayload(EnvelopeOptions options) + { + PayloadContent = JsonSerializer.Deserialize(Payload, options.JsonOptions); + } +} + +public record EventMetadata(string EventId, DateTime Timestamp); +public record MyData(string Name, int Value); +``` + +**Usage:** + +```csharp title="Program.cs" linenums="1" +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +lambda.MapHandler(([Event] CustomRequestEvent request, ILogger logger) => +{ + if (request.PayloadContent is null) + { + logger.LogError("Failed to deserialize payload"); + return new { Error = "Invalid payload" }; + } + + logger.LogInformation( + "Processing event {EventId}: {Name} = {Value}", + request.Metadata.EventId, + request.PayloadContent.Name, + request.PayloadContent.Value + ); + + return new { Success = true }; +}); + +await lambda.RunAsync(); +``` + +--- + +### Example 2: Request/Response Envelope Pair + +Custom envelopes for both incoming requests and outgoing responses: + +```csharp title="ApiEnvelopes.cs" linenums="1" +using System.Text.Json; +using System.Text.Json.Serialization; +using AwsLambda.Host.Abstractions.Options; +using AwsLambda.Host.Envelopes; + +// Request envelope +public class ApiRequest : IRequestEnvelope +{ + [JsonPropertyName("body")] + public required string Body { get; set; } + + [JsonIgnore] + public RequestPayload? BodyContent { get; set; } + + public void ExtractPayload(EnvelopeOptions options) + { + BodyContent = JsonSerializer.Deserialize(Body, options.JsonOptions); + } +} + +// Response envelope +public class ApiResponse : IResponseEnvelope +{ + [JsonPropertyName("statusCode")] + public int StatusCode { get; set; } + + [JsonPropertyName("body")] + public string? Body { get; set; } + + [JsonIgnore] + public ResponsePayload? BodyContent { get; set; } + + public void PackPayload(EnvelopeOptions options) + { + Body = JsonSerializer.Serialize(BodyContent, options.JsonOptions); + } +} + +public record RequestPayload(string Action, Dictionary Parameters); +public record ResponsePayload(bool Success, string Message, object? Data = null); +``` + +**Usage:** + +```csharp title="Program.cs" linenums="1" +lambda.MapHandler(request => +{ + var response = new ApiResponse { StatusCode = 200 }; + + if (request.BodyContent is null) + { + response.StatusCode = 400; + response.BodyContent = new ResponsePayload( + Success: false, + Message: "Invalid request body" + ); + return response; + } + + // Process the request + var result = ProcessAction(request.BodyContent.Action, request.BodyContent.Parameters); + + response.BodyContent = new ResponsePayload( + Success: true, + Message: "Action completed", + Data: result + ); + + return response; +}); +``` + +--- + +### Example 3: Multi-Record Batch Envelope + +A custom event with multiple records, similar to SQS batch processing: + +```csharp title="BatchEvent.cs" linenums="1" +using System.Text.Json; +using System.Text.Json.Serialization; +using AwsLambda.Host.Abstractions.Options; +using AwsLambda.Host.Envelopes; + +public class BatchEvent : IRequestEnvelope +{ + [JsonPropertyName("records")] + public required List Records { get; set; } + + public void ExtractPayload(EnvelopeOptions options) + { + foreach (var record in Records) + { + try + { + record.DataContent = JsonSerializer.Deserialize( + record.Data, + options.JsonOptions + ); + } + catch + { + record.DataContent = null; // Handle deserialization failures gracefully + } + } + } + + public class Record + { + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("data")] + public required string Data { get; set; } // Serialized JSON + + [JsonIgnore] + public MyPayload? DataContent { get; set; } // Deserialized object + } +} + +public record MyPayload(string Type, int Count, DateTime CreatedAt); +``` + +**Usage:** + +```csharp title="Program.cs" linenums="1" +lambda.MapHandler(([Event] BatchEvent batch, ILogger logger) => +{ + var processed = 0; + var failed = 0; + + foreach (var record in batch.Records) + { + if (record.DataContent is null) + { + logger.LogWarning("Failed to deserialize record {RecordId}", record.Id); + failed++; + continue; + } + + logger.LogInformation( + "Processing record {RecordId}: {Type} x{Count}", + record.Id, + record.DataContent.Type, + record.DataContent.Count + ); + + ProcessRecord(record.DataContent); + processed++; + } + + return new { Processed = processed, Failed = failed }; +}); +``` + +--- + +### Best Practices + +#### ✅ Do + +- **Check for null after extraction** - Deserialization can fail +- **Use `[JsonIgnore]` on content properties** - Prevents circular serialization +- **Handle exceptions gracefully** - Set content to `null` on deserialization errors +- **Use `EnvelopeOptions`** - Access configured JSON/XML settings through the `options` parameter +- **Log deserialization failures** - Helps with debugging malformed payloads + +#### ❌ Don't + +- **Don't assume `PayloadContent` is non-null** - Always check before using +- **Don't throw exceptions in `ExtractPayload`** - Handle errors gracefully +- **Don't modify the original payload string** - Only populate the deserialized content property + +--- + +### Key Concepts + +1. **Automatic Invocation** - The framework calls `ExtractPayload()` and `PackPayload()` automatically via middleware + +2. **Separation of Concerns** - Keep serialized strings and deserialized objects separate using `[JsonIgnore]` + +3. **Configuration Access** - Use `options.JsonOptions`, `options.XmlReaderSettings`, etc. for consistent serialization settings + +4. **Error Handling** - Set content properties to `null` when deserialization fails rather than throwing exceptions + +--- + +## Choosing the Right Feature + +### When to Use Envelopes + +Use envelope packages when: + +- ✅ You need type-safe access to message payloads from AWS event sources +- ✅ You want compile-time type checking for event data +- ✅ You're tired of manually parsing JSON from event bodies +- ✅ You need custom serialization formats (XML, Protobuf, etc.) +- ✅ You want IDE IntelliSense support for message structures + +--- + +## Installation + +### Envelope Packages + +Install only the envelope packages you need: + +```bash +# SQS envelope +dotnet add package AwsLambda.Host.Envelopes.Sqs + +# API Gateway envelope +dotnet add package AwsLambda.Host.Envelopes.ApiGateway + +# Other envelopes... +``` diff --git a/docs/features/envelopes/sqs.md b/docs/features/envelopes/sqs.md deleted file mode 100644 index af996c9f..00000000 --- a/docs/features/envelopes/sqs.md +++ /dev/null @@ -1,709 +0,0 @@ -# SQS Envelope - -Strongly-typed SQS event handling with automatic JSON deserialization of message bodies. - ---- - -## Introduction - -The `AwsLambda.Host.Envelopes.Sqs` package provides type-safe wrappers around AWS Lambda's `SQSEvent` class. Instead of manually parsing JSON from `record.Body` strings, you get strongly-typed access to deserialized message payloads via `record.BodyContent`. - -**When to use**: - -- ✅ Processing SQS messages with structured JSON payloads -- ✅ You want compile-time type safety for message handling -- ✅ You need IDE IntelliSense support for message properties -- ✅ Implementing SNS-to-SQS subscription patterns -- ✅ You want to avoid manual JSON parsing boilerplate - ---- - -## Installation - -```bash -dotnet add package AwsLambda.Host.Envelopes.Sqs -``` - -Requires `AwsLambda.Host` to be installed. - ---- - -## Classes Provided - -| Class | Base Class | Use Case | -|-------|-----------|----------| -| **`SqsEnvelope`** | `SQSEvent` | SQS messages with deserialized JSON message bodies | -| **`SqsSnsEnvelope`** | `SqsEnvelopeBase` | SQS messages containing SNS notifications (SNS-to-SQS pattern) | - -Both classes extend `SqsEnvelopeBase`, which provides the core envelope functionality. - ---- - -## Quick Start - -Define your message type and handler: - -```csharp title="Program.cs" linenums="1" -using Amazon.Lambda.SQSEvents; -using AwsLambda.Host.Builder; -using AwsLambda.Host.Envelopes.Sqs; -using Microsoft.Extensions.Logging; - -var builder = LambdaApplication.CreateBuilder(); -var lambda = builder.Build(); - -// SqsEnvelope provides access to SQS event and deserialized Message payloads -lambda.MapHandler( - ([Event] SqsEnvelope envelope, ILogger logger) => - { - // Return SQSBatchResponse to handle partial failures - var batchResponse = new SQSBatchResponse(); - - foreach (var record in envelope.Records) - { - // Add failure if message body couldn't be deserialized - if (record.BodyContent is null) - { - batchResponse.BatchItemFailures.Add( - new SQSBatchResponse.BatchItemFailure - { - ItemIdentifier = record.MessageId - } - ); - continue; - } - - logger.LogInformation( - "Processing message: {Name}", - record.BodyContent.Name - ); - } - - return batchResponse; - } -); - -await lambda.RunAsync(); - -// Your message payload - deserialized from SQS message body -internal record Message(string Name); -``` - ---- - -## Request Envelope (`SqsEnvelope`) - -### Basic Usage - -The `SqsEnvelope` class extends `SQSEvent` and provides type-safe access to deserialized message payloads: - -```csharp title="Program.cs" linenums="1" -lambda.MapHandler( - ([Event] SqsEnvelope envelope, IOrderProcessor processor) => - { - foreach (var record in envelope.Records) - { - if (record.BodyContent is null) - { - Console.WriteLine($"Failed to deserialize message {record.MessageId}"); - continue; - } - - // Access strongly-typed properties - processor.ProcessOrder( - record.BodyContent.OrderId, - record.BodyContent.CustomerId, - record.BodyContent.TotalAmount - ); - - // Access original SQS message properties - Console.WriteLine($"Message ID: {record.MessageId}"); - Console.WriteLine($"Receipt Handle: {record.ReceiptHandle}"); - Console.WriteLine($"Sent Timestamp: {record.Attributes["SentTimestamp"]}"); - } - - return new SQSBatchResponse(); - } -); - -internal record OrderMessage( - string OrderId, - string CustomerId, - decimal TotalAmount, - DateTime OrderDate -); -``` - -### Accessing Messages - -Each message in `envelope.Records` has: - -- **`BodyContent`** (type `T?`) - Deserialized message payload (null if deserialization failed) -- **All `SQSMessage` properties** - `MessageId`, `ReceiptHandle`, `Body`, `Attributes`, `MessageAttributes`, etc. - -```csharp title="Accessing message properties" -foreach (var record in envelope.Records) -{ - // Deserialized payload - var payload = record.BodyContent; // Type: OrderMessage? - - // Original SQS message properties - var messageId = record.MessageId; - var receiptHandle = record.ReceiptHandle; - var rawBody = record.Body; // Original JSON string - var attributes = record.Attributes; - var messageAttributes = record.MessageAttributes; -} -``` - -### Batch Response Handling - -SQS supports partial batch failures. Return `SQSBatchResponse` with failed message IDs: - -```csharp title="Partial batch failure handling" linenums="1" -lambda.MapHandler( - ([Event] SqsEnvelope envelope) => - { - var batchResponse = new SQSBatchResponse(); - - foreach (var record in envelope.Records) - { - try - { - if (record.BodyContent is null) - throw new InvalidOperationException("Deserialization failed"); - - // Process message - ProcessMessage(record.BodyContent); - } - catch (Exception ex) - { - Console.WriteLine($"Error processing {record.MessageId}: {ex.Message}"); - - // Mark message as failed - will be retried - batchResponse.BatchItemFailures.Add( - new SQSBatchResponse.BatchItemFailure - { - ItemIdentifier = record.MessageId - } - ); - } - } - - return batchResponse; - } -); -``` - -!!! tip "Partial Batch Failure Best Practices" - - Always return `SQSBatchResponse` even if all messages succeed (empty failures list) - - Add failed message IDs to `BatchItemFailures` - SQS will retry only those messages - - Failed messages are made visible again and will be redelivered - - Successful messages are deleted from the queue - ---- - -## SNS-to-SQS Pattern (`SqsSnsEnvelope`) - -### When to Use - -Use `SqsSnsEnvelope` when: - -- SNS topic delivers messages to an SQS queue (SNS-to-SQS subscription) -- Your Lambda function processes messages from that SQS queue -- You need to access both SNS message metadata AND the typed payload - -### Two-Stage Deserialization - -`SqsSnsEnvelope` performs two-stage deserialization: - -1. **Stage 1**: SQS message body → SNS message envelope -2. **Stage 2**: SNS message content → Your payload type `T` - -```csharp title="SNS-to-SQS pattern" linenums="1" -using AwsLambda.Host.Envelopes.Sns; - -lambda.MapHandler( - ([Event] SqsSnsEnvelope envelope, ILogger logger) => - { - var batchResponse = new SQSBatchResponse(); - - foreach (var record in envelope.Records) - { - if (record.BodyContent is null) - { - batchResponse.BatchItemFailures.Add( - new SQSBatchResponse.BatchItemFailure - { - ItemIdentifier = record.MessageId - } - ); - continue; - } - - // Access SNS message envelope - var snsMessage = record.BodyContent; - - // Access deserialized SNS message content - var notification = snsMessage.MessageContent; - - logger.LogInformation( - "SNS Topic: {Topic}, Subject: {Subject}, Message: {Message}", - snsMessage.TopicArn, - snsMessage.Subject, - notification?.Message - ); - } - - return batchResponse; - } -); - -internal record Notification(string Message, DateTime Timestamp); -``` - -### SNS Message Properties - -The `BodyContent` provides access to SNS message envelope: - -```csharp -foreach (var record in envelope.Records) -{ - var snsEnvelope = record.BodyContent; // Type: SnsMessageEnvelope? - - // SNS message metadata - var topicArn = snsEnvelope.TopicArn; - var subject = snsEnvelope.Subject; - var messageId = snsEnvelope.MessageId; - var timestamp = snsEnvelope.Timestamp; - var messageAttributes = snsEnvelope.MessageAttributes; - - // Deserialized payload from SNS message - var payload = snsEnvelope.MessageContent; // Type: Notification? -} -``` - ---- - -## Configuration - -### Envelope Options - -Configure JSON serialization options for envelope payloads: - -```csharp title="Program.cs" linenums="1" -using System.Text.Json; - -var builder = LambdaApplication.CreateBuilder(); - -builder.Services.ConfigureEnvelopeOptions(options => -{ - // Use snake_case for JSON property names - options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - - // Case-insensitive property matching - options.JsonOptions.PropertyNameCaseInsensitive = true; - - // Allow trailing commas in JSON - options.JsonOptions.AllowTrailingCommas = true; -}); - -var lambda = builder.Build(); -``` - -### Common Configuration Patterns - -```csharp title="Configuration examples" linenums="1" -builder.Services.ConfigureEnvelopeOptions(options => -{ - // Camel case properties - options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - - // Write indented JSON for debugging - options.JsonOptions.WriteIndented = true; - - // Handle missing properties gracefully - options.JsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - - // Custom number handling - options.JsonOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; -}); -``` - ---- - -## Native AOT Support - -### JsonSerializerContext Registration - -For Native AOT compilation, register envelope and payload types in your `JsonSerializerContext`: - -```csharp title="Program.cs" linenums="1" -using System.Text.Json.Serialization; - -[JsonSerializable(typeof(SqsEnvelope))] -[JsonSerializable(typeof(Message))] -internal partial class SerializerContext : JsonSerializerContext; -``` - -### Dual Context Registration - -**Critical**: Register the context in **both** the Lambda serializer AND envelope options: - -```csharp title="Program.cs" linenums="1" -var builder = LambdaApplication.CreateBuilder(); - -// 1. Register for Lambda event deserialization -builder.Services.AddLambdaSerializerWithContext(); - -// 2. Register for envelope payload deserialization -builder.Services.ConfigureEnvelopeOptions(options => -{ - options.JsonOptions.TypeInfoResolver = SerializerContext.Default; -}); - -var lambda = builder.Build(); -``` - -!!! warning "Why Two Registrations?" - The context must be registered twice because deserialization happens in two stages: - - 1. **Lambda serializer** deserializes the raw `SQSEvent` from Lambda runtime - 2. **Envelope options** deserialize the message body content into your payload type `T` - - Both stages need access to the `JsonSerializerContext` for AOT compilation. - -### SNS-to-SQS AOT Configuration - -For `SqsSnsEnvelope`, you must register additional types with unique `TypeInfoPropertyName`: - -```csharp title="SNS-to-SQS AOT context" linenums="1" -using Amazon.Lambda.SNSEvents; -using Amazon.Lambda.SQSEvents; - -[JsonSerializable(typeof(SqsSnsEnvelope))] -[JsonSerializable(typeof(SnsEnvelope.SnsMessageEnvelope))] -[JsonSerializable(typeof(SNSEvent.MessageAttribute), TypeInfoPropertyName = "SnsMessageAttribute")] -[JsonSerializable(typeof(SQSEvent.MessageAttribute), TypeInfoPropertyName = "SqsMessageAttribute")] -[JsonSerializable(typeof(Notification))] -internal partial class SerializerContext : JsonSerializerContext; -``` - -!!! danger "MessageAttribute Naming Collision" - Both `SNSEvent.MessageAttribute` and `SQSEvent.MessageAttribute` have the same type name. Without unique `TypeInfoPropertyName` values, AOT compilation will fail with a naming conflict. Always specify unique names for these types. - ---- - -## Custom Envelopes - -### XML Serialization Example - -Extend `SqsEnvelopeBase` to support custom serialization formats: - -```csharp title="SqsXmlEnvelope.cs" linenums="1" -using System.Xml; -using System.Xml.Serialization; -using AwsLambda.Host.Abstractions.Options; -using AwsLambda.Host.Envelopes.Sqs; - -public sealed class SqsXmlEnvelope : SqsEnvelopeBase -{ - private static readonly XmlSerializer Serializer = new(typeof(T)); - - public override void ExtractPayload(EnvelopeOptions options) - { - foreach (var record in Records) - { - using var stringReader = new StringReader(record.Body); - using var xmlReader = XmlReader.Create(stringReader, options.XmlReaderSettings); - record.BodyContent = (T?)Serializer.Deserialize(xmlReader); - } - } -} -``` - -**Usage**: - -```csharp title="Using XML envelope" -lambda.MapHandler( - ([Event] SqsXmlEnvelope envelope) => - { - foreach (var record in envelope.Records) - { - Console.WriteLine($"Deserialized XML: {record.BodyContent?.Data}"); - } - - return new SQSBatchResponse(); - } -); - -public class XmlMessage -{ - public string? Data { get; set; } -} -``` - -### Custom Serialization Formats - -You can implement any serialization format by overriding `ExtractPayload`: - -```csharp title="Protocol Buffers example (conceptual)" -public sealed class SqsProtobufEnvelope : SqsEnvelopeBase where T : IMessage, new() -{ - public override void ExtractPayload(EnvelopeOptions options) - { - var parser = new MessageParser(() => new T()); - - foreach (var record in Records) - { - var bytes = Convert.FromBase64String(record.Body); - record.BodyContent = parser.ParseFrom(bytes); - } - } -} -``` - ---- - -## Common Patterns - -### Validation - -Validate deserialized payloads before processing: - -```csharp title="Payload validation" linenums="1" -lambda.MapHandler( - ([Event] SqsEnvelope envelope, IValidator validator) => - { - var batchResponse = new SQSBatchResponse(); - - foreach (var record in envelope.Records) - { - if (record.BodyContent is null) - { - Console.WriteLine($"Deserialization failed for {record.MessageId}"); - batchResponse.BatchItemFailures.Add( - new SQSBatchResponse.BatchItemFailure - { - ItemIdentifier = record.MessageId - } - ); - continue; - } - - var validationResult = validator.Validate(record.BodyContent); - if (!validationResult.IsValid) - { - Console.WriteLine($"Validation failed: {string.Join(", ", validationResult.Errors)}"); - batchResponse.BatchItemFailures.Add( - new SQSBatchResponse.BatchItemFailure - { - ItemIdentifier = record.MessageId - } - ); - continue; - } - - // Process valid message - ProcessOrder(record.BodyContent); - } - - return batchResponse; - } -); -``` - -### Error Handling with Logging - -```csharp title="Error handling pattern" linenums="1" -lambda.MapHandler( - ([Event] SqsEnvelope envelope, ILogger logger) => - { - var batchResponse = new SQSBatchResponse(); - - foreach (var record in envelope.Records) - { - try - { - if (record.BodyContent is null) - { - logger.LogError( - "Failed to deserialize message {MessageId}. Body: {Body}", - record.MessageId, - record.Body - ); - throw new InvalidOperationException("Deserialization failed"); - } - - // Process message - ProcessMessage(record.BodyContent); - - logger.LogInformation( - "Successfully processed message {MessageId}", - record.MessageId - ); - } - catch (Exception ex) - { - logger.LogError( - ex, - "Error processing message {MessageId}", - record.MessageId - ); - - batchResponse.BatchItemFailures.Add( - new SQSBatchResponse.BatchItemFailure - { - ItemIdentifier = record.MessageId - } - ); - } - } - - return batchResponse; - } -); -``` - -### Accessing SQS Message Attributes - -```csharp title="Reading message attributes" linenums="1" -foreach (var record in envelope.Records) -{ - // Standard SQS attributes - var sentTimestamp = record.Attributes.GetValueOrDefault("SentTimestamp"); - var approximateReceiveCount = record.Attributes.GetValueOrDefault("ApproximateReceiveCount"); - - // Custom message attributes - if (record.MessageAttributes.TryGetValue("Priority", out var priorityAttr)) - { - var priority = priorityAttr.StringValue; - Console.WriteLine($"Message priority: {priority}"); - } - - // Process with context - if (int.TryParse(approximateReceiveCount, out var receiveCount)) - { - if (receiveCount > 3) - { - Console.WriteLine($"Message {record.MessageId} has been retried {receiveCount} times"); - // Consider moving to DLQ - } - } -} -``` - ---- - -## Best Practices - -### ✅ Do - -- **Always return `SQSBatchResponse`** even if all messages succeed -- **Check for null `BodyContent`** before processing (deserialization can fail) -- **Use partial batch failures** to retry only failed messages -- **Register types for AOT** when using Native AOT compilation -- **Register context in both places** for AOT (Lambda serializer + envelope options) -- **Log deserialization failures** with message ID and body for debugging -- **Use message attributes** for filtering and routing -- **Implement idempotency** - SQS may deliver messages more than once - -### ❌ Don't - -- **Don't throw unhandled exceptions** - use `SQSBatchResponse.BatchItemFailures` instead -- **Don't process without null checks** - `BodyContent` can be null if deserialization fails -- **Don't retry all messages** when only some fail - use partial batch failures -- **Don't forget dual context registration** for AOT (will cause runtime errors) -- **Don't ignore `ApproximateReceiveCount`** - monitor for poison pill messages -- **Don't skip DLQ configuration** - messages failing repeatedly should go to DLQ - ---- - -## Troubleshooting - -### "`BodyContent` is always null" - -**Cause**: JSON deserialization is failing silently. - -**Solutions**: - -1. Check your message payload structure matches the type definition: - ```csharp - // Ensure property names match (or use [JsonPropertyName]) - internal record Message( - [property: JsonPropertyName("message_name")] string MessageName - ); - ``` - -2. Enable case-insensitive matching: - ```csharp - builder.Services.ConfigureEnvelopeOptions(options => - { - options.JsonOptions.PropertyNameCaseInsensitive = true; - }); - ``` - -3. Log raw message body to inspect structure: - ```csharp - if (record.BodyContent is null) - { - Console.WriteLine($"Failed to deserialize: {record.Body}"); - } - ``` - -### "AOT compilation fails with naming conflict" - -**Error**: `error CS0102: The type 'JsonContext' already contains a definition for 'MessageAttribute'` - -**Cause**: Both `SNSEvent.MessageAttribute` and `SQSEvent.MessageAttribute` registered without unique names. - -**Solution**: Use `TypeInfoPropertyName` for `SqsSnsEnvelope`: -```csharp -[JsonSerializable(typeof(SNSEvent.MessageAttribute), TypeInfoPropertyName = "SnsMessageAttribute")] -[JsonSerializable(typeof(SQSEvent.MessageAttribute), TypeInfoPropertyName = "SqsMessageAttribute")] -``` - -### "Messages retrying indefinitely" - -**Cause**: Not using `SQSBatchResponse` to mark failures, or throwing exceptions instead. - -**Solution**: Always return `SQSBatchResponse` with failed message IDs: -```csharp -var batchResponse = new SQSBatchResponse(); - -try -{ - ProcessMessage(record.BodyContent); -} -catch -{ - batchResponse.BatchItemFailures.Add( - new SQSBatchResponse.BatchItemFailure { ItemIdentifier = record.MessageId } - ); -} - -return batchResponse; // Don't throw! -``` - ---- - -## Key Takeaways - -1. **Type-safe payloads** - `SqsEnvelope` eliminates manual JSON parsing -2. **Two classes** - `SqsEnvelope` for SQS, `SqsSnsEnvelope` for SNS-to-SQS -3. **Partial batch failures** - Return `SQSBatchResponse` with failed message IDs -4. **Null checks required** - `BodyContent` can be null if deserialization fails -5. **AOT needs two registrations** - Lambda serializer AND envelope options -6. **Custom formats supported** - Extend `SqsEnvelopeBase` for XML, Protobuf, etc. -7. **SNS-to-SQS requires special handling** - Unique `TypeInfoPropertyName` for MessageAttribute types - ---- - -## Next Steps - -- **[Envelope Pattern Overview](index.md)** - Learn how all envelopes work -- **[SNS Envelope](sns.md)** - Process SNS notifications with type safety -- **[API Gateway Envelope](api-gateway.md)** - Handle API Gateway requests/responses -- **[Configuration Guide](../../guides/configuration.md)** - Configure envelope serialization options -- **[Error Handling Guide](../../guides/error-handling.md)** - Error handling patterns for Lambda -- **[Deployment Guide](../../guides/deployment.md)** - Deploy SQS-triggered Lambda functions diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md new file mode 100644 index 00000000..8345943c --- /dev/null +++ b/docs/features/open_telemetry.md @@ -0,0 +1,79 @@ +# OpenTelemetry + +**What is OpenTelemetry Integration?** + +The `AwsLambda.Host.OpenTelemetry` package provides comprehensive observability integration for distributed tracing and metrics collection in AWS Lambda functions. + +**Capabilities**: + +- **Distributed Tracing** - Automatic span creation and context propagation for Lambda invocations +- **Metrics Collection** - Performance and business metrics exportable to standard observability backends +- **AWS Lambda Instrumentation** - Lambda-specific insights including cold starts, warm invocations, and error tracking +- **Lifecycle Integration** - Seamless integration with OnInit, Invocation, and OnShutdown phases +- **Vendor-Neutral** - Built on the OpenTelemetry SDK for compatibility with any observability backend + +**Supported Exporters**: + +- OTLP (OpenTelemetry Protocol) +- Jaeger +- AWS X-Ray +- Datadog +- New Relic +- CloudWatch +- And more... + +!!! info "Learn More About OpenTelemetry" + - **[OpenTelemetry Integration Guide](opentelemetry.md)** - Complete setup and configuration + +--- + +## Quick Start + +### Using OpenTelemetry + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.Builder; +using AwsLambda.Host.OpenTelemetry; + +var builder = LambdaApplication.CreateBuilder(); + +// Add OpenTelemetry tracing and metrics +builder.Services.AddLambdaOpenTelemetry(); + +var lambda = builder.Build(); + +lambda.MapHandler(([Event] Request request) => +{ + // Automatic span creation for this invocation + return new Response($"Hello {request.Name}!"); +}); + +await lambda.RunAsync(); + +internal record Request(string Name); +internal record Response(string Message); +``` + +--- + +## Choosing the Right Feature + +### When to Use OpenTelemetry + +Use the OpenTelemetry package when: + +- ✅ You need distributed tracing across microservices +- ✅ You want to monitor Lambda performance and cold starts +- ✅ You need to export metrics to observability platforms (Datadog, New Relic, etc.) +- ✅ You want automatic instrumentation for Lambda invocations +- ✅ You're building complex serverless architectures + +--- + +## Installation + +### OpenTelemetry Package + +```bash +dotnet add package AwsLambda.Host.OpenTelemetry +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 6625fbbd..be59b0fb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -111,7 +111,7 @@ nav: - CloudWatch Logs: features/envelopes/cloudwatch-logs.md - ALB: features/envelopes/alb.md - Custom Envelopes: features/envelopes/custom-envelopes.md - - OpenTelemetry: features/opentelemetry.md + - OpenTelemetry: features/open_telemetry.md plugins: - search From d17a5b8cb3199a06077bf0cfbc67ff0042b74a12 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 14:16:21 -0500 Subject: [PATCH 19/65] chore(docs): consolidate envelope section in navigation - Merged envelope sub-sections into a single `features/envelopes.md` for improved organization. - Removed individual entries for SQS, SNS, and other envelope types from `mkdocs.yml`. - Updated documentation structure to streamline navigation and simplify content discovery. --- docs/features/{envelopes/index.md => envelopes.md} | 0 mkdocs.yml | 12 +----------- 2 files changed, 1 insertion(+), 11 deletions(-) rename docs/features/{envelopes/index.md => envelopes.md} (100%) diff --git a/docs/features/envelopes/index.md b/docs/features/envelopes.md similarity index 100% rename from docs/features/envelopes/index.md rename to docs/features/envelopes.md diff --git a/mkdocs.yml b/mkdocs.yml index be59b0fb..07312f22 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -100,17 +100,7 @@ nav: - Deployment: guides/deployment.md - Features: - features/index.md - - Envelopes: - - features/envelopes/index.md - - SQS: features/envelopes/sqs.md - - SNS: features/envelopes/sns.md - - API Gateway: features/envelopes/api-gateway.md - - Kinesis: features/envelopes/kinesis.md - - Kinesis Firehose: features/envelopes/kinesis-firehose.md - - Kafka: features/envelopes/kafka.md - - CloudWatch Logs: features/envelopes/cloudwatch-logs.md - - ALB: features/envelopes/alb.md - - Custom Envelopes: features/envelopes/custom-envelopes.md + - Envelopes: features/envelopes.md - OpenTelemetry: features/open_telemetry.md plugins: From eedeacbf32056ee4e6b11eb74f234f10f7f3f859 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 15:18:40 -0500 Subject: [PATCH 20/65] chore(docs): streamline features overview and consolidate content - Simplified `docs/features/index.md` by condensing envelope and observability sections. - Removed redundant examples and details, centralizing key information for clarity. - Updated links to reference consolidated envelope and observability documentation. --- docs/features/index.md | 235 ++--------------------------------------- 1 file changed, 6 insertions(+), 229 deletions(-) diff --git a/docs/features/index.md b/docs/features/index.md index a705448c..06dfbcb2 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -1,241 +1,18 @@ # Features -The aws-lambda-host framework provides a rich ecosystem of features and extension packages that enhance AWS Lambda development beyond the core framework capabilities. +The `AwsLambda.Host` framework provides a rich ecosystem of features and extension packages that enhance AWS Lambda development beyond the core framework capabilities. These features are designed to be modular, type-safe, and performant, integrating seamlessly with the core hosting patterns. ---- - -## Core Framework vs Features - -The framework is organized into two main categories: - -### Core Framework (`AwsLambda.Host`) - -The core framework provides the foundational hosting capabilities: - -- **.NET Hosting Patterns** - Middleware, builder pattern, and dependency injection (similar to ASP.NET Core) -- **Async-First Design** - Native async/await support with Lambda timeout and cancellation handling -- **Source Generators & Interceptors** - Compile-time code generation for optimal performance -- **Flexible Handler Registration** - Simple, declarative API with the `[Event]` attribute -- **Lifecycle Management** - OnInit, Invocation, and OnShutdown phases - -Learn more in the **[Guides](../guides/index.md)** section. - -### Features & Extensions - -Features extend the core framework with specialized capabilities for specific use cases: - -- **Envelopes** - Type-safe event source integration packages -- **Observability** - OpenTelemetry integration for distributed tracing and metrics +This section provides an overview of the available features. --- ## Feature Categories -### 1. Envelope Pattern - -**What are Envelopes?** - -Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) and add a `BodyContent` property that provides type-safe access to deserialized message payloads. Instead of manually parsing JSON strings from event bodies, you get strongly-typed objects with full IDE support and compile-time type checking. - -**Key Benefits**: - -- **Type Safety** - Generic type parameter `` ensures compile-time type checking -- **Extensibility** - Abstract base classes allow custom serialization formats (JSON, XML, etc.) -- **Zero Overhead** - Envelopes extend official AWS event types, adding no runtime cost -- **AOT Ready** - Support for Native AOT compilation via `JsonSerializerContext` registration -- **Familiar API** - Works seamlessly with existing AWS Lambda event patterns - -**Supported Event Sources**: - -| Event Source | Package | Use Case | -|--------------------------------------|--------------------------------------------------------------------------|---------------------------------------------------------| -| **SQS** | [AwsLambda.Host.Envelopes.Sqs](envelopes/sqs.md) | Queue message processing with type-safe payloads | -| **SNS** | [AwsLambda.Host.Envelopes.Sns](envelopes/sns.md) | Pub/sub notifications with typed messages | -| **API Gateway** | [AwsLambda.Host.Envelopes.ApiGateway](envelopes/api-gateway.md) | REST/HTTP/WebSocket APIs with request/response envelopes| -| **Kinesis Data Streams** | [AwsLambda.Host.Envelopes.Kinesis](envelopes/kinesis.md) | Stream processing with typed records | -| **Kinesis Data Firehose** | [AwsLambda.Host.Envelopes.KinesisFirehose](envelopes/kinesis-firehose.md)| Data transformation with typed payloads | -| **Kafka (MSK or self-managed)** | [AwsLambda.Host.Envelopes.Kafka](envelopes/kafka.md) | Event streaming with typed messages | -| **CloudWatch Logs** | [AwsLambda.Host.Envelopes.CloudWatchLogs](envelopes/cloudwatch-logs.md) | Log processing with typed log events | -| **Application Load Balancer** | [AwsLambda.Host.Envelopes.Alb](envelopes/alb.md) | ALB target integration with request/response envelopes | - -!!! tip "Learn More About Envelopes" - - **[Envelope Pattern Overview](envelopes/index.md)** - Detailed explanation of how envelopes work - - **[Creating Custom Envelopes](envelopes/custom-envelopes.md)** - Build your own envelope implementations - -### 2. Observability (OpenTelemetry) - -**What is OpenTelemetry Integration?** - -The `AwsLambda.Host.OpenTelemetry` package provides comprehensive observability integration for distributed tracing and metrics collection in AWS Lambda functions. - -**Capabilities**: - -- **Distributed Tracing** - Automatic span creation and context propagation for Lambda invocations -- **Metrics Collection** - Performance and business metrics exportable to standard observability backends -- **AWS Lambda Instrumentation** - Lambda-specific insights including cold starts, warm invocations, and error tracking -- **Lifecycle Integration** - Seamless integration with OnInit, Invocation, and OnShutdown phases -- **Vendor-Neutral** - Built on the OpenTelemetry SDK for compatibility with any observability backend - -**Supported Exporters**: - -- OTLP (OpenTelemetry Protocol) -- Jaeger -- AWS X-Ray -- Datadog -- New Relic -- CloudWatch -- And more... - -!!! info "Learn More About OpenTelemetry" - - **[OpenTelemetry Integration Guide](opentelemetry.md)** - Complete setup and configuration - ---- - -## Design Philosophy - -All features in the aws-lambda-host ecosystem follow these principles: - -1. **Zero Runtime Overhead** - Features extend existing types rather than wrapping them -2. **AOT Compatibility** - Full support for Native AOT compilation -3. **Type Safety** - Compile-time type checking wherever possible -4. **Extensibility** - Abstract base classes and interfaces for custom implementations -5. **Familiar .NET Patterns** - Leverage existing .NET idioms and practices - ---- - -## Quick Start - -### Using Envelopes - -```csharp title="Program.cs" linenums="1" -using AwsLambda.Host.Builder; -using AwsLambda.Host.Envelopes.Sqs; - -var builder = LambdaApplication.CreateBuilder(); -var lambda = builder.Build(); - -// Type-safe SQS message processing -lambda.MapHandler( - ([Event] SqsEnvelope envelope, ILogger logger) => - { - foreach (var record in envelope.Records) - { - if (record.BodyContent is null) - continue; - - logger.LogInformation( - "Processing order: {OrderId}", - record.BodyContent.OrderId - ); - } +### [Envelope Pattern](./envelopes.md) - return new SQSBatchResponse(); - } -); +The Envelope pattern provides type-safe wrappers for various AWS event sources like SQS, SNS, and API Gateway. Instead of manually parsing JSON, you can work with strongly-typed objects, improving code quality and developer productivity. -await lambda.RunAsync(); - -internal record OrderMessage(string OrderId, decimal Amount); -``` - -### Using OpenTelemetry - -```csharp title="Program.cs" linenums="1" -using AwsLambda.Host.Builder; -using AwsLambda.Host.OpenTelemetry; - -var builder = LambdaApplication.CreateBuilder(); - -// Add OpenTelemetry tracing and metrics -builder.Services.AddLambdaOpenTelemetry(); - -var lambda = builder.Build(); - -lambda.MapHandler(([Event] Request request) => -{ - // Automatic span creation for this invocation - return new Response($"Hello {request.Name}!"); -}); - -await lambda.RunAsync(); - -internal record Request(string Name); -internal record Response(string Message); -``` - ---- - -## Choosing the Right Feature - -### When to Use Envelopes - -Use envelope packages when: - -- ✅ You need type-safe access to message payloads from AWS event sources -- ✅ You want compile-time type checking for event data -- ✅ You're tired of manually parsing JSON from event bodies -- ✅ You need custom serialization formats (XML, Protobuf, etc.) -- ✅ You want IDE IntelliSense support for message structures - -### When to Use OpenTelemetry - -Use the OpenTelemetry package when: - -- ✅ You need distributed tracing across microservices -- ✅ You want to monitor Lambda performance and cold starts -- ✅ You need to export metrics to observability platforms (Datadog, New Relic, etc.) -- ✅ You want automatic instrumentation for Lambda invocations -- ✅ You're building complex serverless architectures - ---- - -## Installation - -### Core Framework (Required) - -All features require the core framework: - -```bash -dotnet add package AwsLambda.Host -``` - -### Envelope Packages (Optional) - -Install only the envelope packages you need: - -```bash -# SQS envelope -dotnet add package AwsLambda.Host.Envelopes.Sqs - -# API Gateway envelope -dotnet add package AwsLambda.Host.Envelopes.ApiGateway - -# Other envelopes... -``` - -### OpenTelemetry Package (Optional) - -```bash -dotnet add package AwsLambda.Host.OpenTelemetry -``` - ---- - -## Next Steps - -Explore specific features: - -- **[Envelope Pattern](envelopes/index.md)** - Learn how envelopes work and browse supported event sources -- **[OpenTelemetry Integration](opentelemetry.md)** - Set up distributed tracing and metrics -- **[Guides](../guides/index.md)** - Learn about core framework capabilities -- **[Examples](../examples/index.md)** - See complete examples using features - ---- +### [Observability (OpenTelemetry)](./open_telemetry.md) -## Key Takeaways +This feature provides comprehensive observability through OpenTelemetry integration. It enables distributed tracing and metrics collection, offering deep insights into your Lambda function's performance and behavior. -1. **Features extend the core** - All features build on top of `AwsLambda.Host` -2. **Zero overhead design** - Envelopes extend official AWS types with no runtime cost -3. **Pick what you need** - Install only the envelope packages relevant to your use case -4. **AOT compatible** - All features support Native AOT compilation -5. **Type-safe by default** - Envelopes provide compile-time type checking for event payloads From 1e358af434b495d97778cae8579d4fbc360edea5 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 16:14:57 -0500 Subject: [PATCH 21/65] chore(docs): enhance OpenTelemetry integration guide - Improved structure and organization of the OpenTelemetry documentation. - Clarified lifecycle integration with detailed examples for `OnInit`, `Invocation`, and `OnShutdown`. - Added sections on custom instrumentation for spans and metrics. - Expanded configuration examples for OTLP, X-Ray, and Prometheus exporters. - Included a practical guide for viewing traces with Jaeger and Lambda Test Tool. --- docs/features/open_telemetry.md | 265 ++++++++++++++++++++++++++++---- 1 file changed, 233 insertions(+), 32 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 8345943c..8d3e1b67 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -2,49 +2,52 @@ **What is OpenTelemetry Integration?** -The `AwsLambda.Host.OpenTelemetry` package provides comprehensive observability integration for distributed tracing and metrics collection in AWS Lambda functions. +The `AwsLambda.Host.OpenTelemetry` package provides comprehensive observability for your serverless application by integrating distributed tracing and metrics collection directly into the AWS Lambda lifecycle. It builds on the vendor-neutral OpenTelemetry SDK, allowing you to export telemetry data to any compatible observability backend. -**Capabilities**: - -- **Distributed Tracing** - Automatic span creation and context propagation for Lambda invocations -- **Metrics Collection** - Performance and business metrics exportable to standard observability backends -- **AWS Lambda Instrumentation** - Lambda-specific insights including cold starts, warm invocations, and error tracking -- **Lifecycle Integration** - Seamless integration with OnInit, Invocation, and OnShutdown phases -- **Vendor-Neutral** - Built on the OpenTelemetry SDK for compatibility with any observability backend - -**Supported Exporters**: +--- -- OTLP (OpenTelemetry Protocol) -- Jaeger -- AWS X-Ray -- Datadog -- New Relic -- CloudWatch -- And more... +## Key Benefits -!!! info "Learn More About OpenTelemetry" - - **[OpenTelemetry Integration Guide](opentelemetry.md)** - Complete setup and configuration +- **Automatic Instrumentation**: Automatically creates trace spans for the entire Lambda lifecycle, including `OnInit` (cold starts), handler invocation, and `OnShutdown`. +- **Distributed Tracing**: Propagates trace context across services, giving you end-to-end visibility in a microservices architecture. +- **Metrics Collection**: Capture and export performance and business metrics. +- **Lifecycle Integration**: Seamlessly hooks into the `AwsLambda.Host` lifecycle for accurate cold start detection and telemetry flushing. +- **Vendor-Neutral**: Compatible with any OpenTelemetry-compliant backend, including Jaeger, Datadog, New Relic, Honeycomb, and AWS X-Ray. +- **Custom Instrumentation**: Easily create custom spans and metrics to capture application-specific logic. --- ## Quick Start -### Using OpenTelemetry +This example demonstrates how to add basic OpenTelemetry instrumentation. With just a few lines of code, you get automatic tracing for your Lambda handler. ```csharp title="Program.cs" linenums="1" +using System.Diagnostics; using AwsLambda.Host.Builder; using AwsLambda.Host.OpenTelemetry; +using OpenTelemetry.Trace; var builder = LambdaApplication.CreateBuilder(); -// Add OpenTelemetry tracing and metrics +// 1. Add and configure OpenTelemetry +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing + .AddSource("MyCustomSource") // Add a custom source for tracing + .AddConsoleExporter()); // Export traces to the console for debugging + +// 2. Add the Lambda-specific OpenTelemetry instrumentation builder.Services.AddLambdaOpenTelemetry(); var lambda = builder.Build(); -lambda.MapHandler(([Event] Request request) => +// 3. The handler invocation will be automatically traced +lambda.MapHandler(([Event] Request request, ILogger logger) => { - // Automatic span creation for this invocation + // 4. (Optional) Create a custom activity span + using var activity = new ActivitySource("MyCustomSource").StartActivity("ProcessingRequest"); + activity?.SetTag("name", request.Name); + + logger.LogInformation("Responding to {Name}", request.Name); return new Response($"Hello {request.Name}!"); }); @@ -56,24 +59,222 @@ internal record Response(string Message); --- -## Choosing the Right Feature +## Configuration + +Configuration is done using the standard OpenTelemetry .NET SDK extension methods on `IServiceCollection`. The `AddLambdaOpenTelemetry()` call then wires up the instrumentation with the Lambda lifecycle. + +### Basic Configuration (OTLP Exporter) + +The most common scenario is exporting telemetry to an observability platform via the OpenTelemetry Protocol (OTLP). + +```csharp title="Program.cs" linenums="1" +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing + .AddAWSLambdaConfigurations() // Adds Lambda-specific resource attributes + .AddOtlpExporter()) // Exports traces via OTLP + .WithMetrics(metrics => metrics + .AddAWSLambdaConfigurations() // Adds Lambda-specific resource attributes + .AddOtlpExporter()); // Exports metrics via OTLP + +builder.Services.AddLambdaOpenTelemetry(); + +var lambda = builder.Build(); +// ... +``` + +!!! tip "OTLP Endpoint Configuration" + The OTLP exporter endpoint is configured via the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. + +### Configuring Tracing + +You can configure tracing providers, add custom sources, and define sampling rules. + +```csharp title="Program.cs" linenums="1" +using OpenTelemetry.Trace; + +// ... + +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing + .AddAWSLambdaConfigurations() + .AddSource("MyApplication.*") // Listen to custom ActivitySources + .SetSampler(new AlwaysOnSampler()) // Configure sampling + .AddOtlpExporter()); +``` + +### Configuring Metrics + +Configure metrics providers and add custom meters to record application-specific measurements. + +```csharp title="Program.cs" linenums="1" +using OpenTelemetry.Metrics; + +// ... + +builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => metrics + .AddAWSLambdaConfigurations() + .AddMeter("MyApplication.Metrics") // Listen to custom Meters + .AddPrometheusExporter()); // Or any other exporter +``` + +### AWS X-Ray Integration + +To integrate with AWS X-Ray, use the AWS-provided instrumentation and propagator. + +```csharp title="Program.cs" linenums="1" +using OpenTelemetry.Trace; + +// ... + +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => + { + tracing + .AddAWSLambdaConfigurations() + .AddAWSXRayTraceId() // Use X-Ray format for Trace IDs + .AddOtlpExporter(); + + // Propagate X-Ray context + Sdk.SetDefaultTextMapPropagator(new AWSXRayPropagator()); + }); +``` + +--- + +## Lifecycle Integration + +`AwsLambda.Host.OpenTelemetry` automatically instruments the key phases of the Lambda execution lifecycle. + +- **`OnInit`**: The application startup phase (`OnInitAsync`) is wrapped in its own trace span. This allows you to precisely measure cold start times by analyzing the duration of the `OnInit` span. + +- **Invocation**: Each handler invocation is automatically traced. The trace captures the full execution time, including any middleware in the pipeline. + +- **`OnShutdown`**: The `OnShutdownAsync` phase is traced. Importantly, the OpenTelemetry `TracerProvider` is flushed during this phase, ensuring that all buffered telemetry is sent before the execution environment is frozen. + +--- + +## Custom Instrumentation + +To get the most out of observability, you should add custom instrumentation to your application code. + +### Custom Spans (Activities) + +Inject an `ActivitySource` to create custom spans that represent specific units of work, like a database call or an API request. + +```csharp title="NameService.cs" linenums="1" +using System.Diagnostics; + +public class NameService +{ + private static readonly ActivitySource Source = new("MyApplication.NameService"); + + public string GetFullName(string name) + { + using var activity = Source.StartActivity("GetFullName"); + activity?.SetTag("input.name", name); + + // ... some work ... + + var fullName = $"{name} Smith"; + activity?.SetTag("output.fullname", fullName); + + return fullName; + } +} +``` + +### Custom Metrics + +Inject a `Meter` and create `Counter`, `Histogram`, or `UpDownCounter` instruments to record business or performance metrics. + +```csharp title="OrderProcessor.cs" linenums="1" +using System.Diagnostics.Metrics; + +public class OrderProcessor +{ + private static readonly Meter Meter = new("MyApplication.Metrics"); + private static readonly Counter OrdersProcessed = Meter.CreateCounter("orders.processed"); + private static readonly Histogram OrderValue = Meter.CreateHistogram("orders.value"); + + public void Process(Order order) + { + // ... process order ... + + OrdersProcessed.Add(1); + OrderValue.Record(order.Value); + } +} +``` + +--- + +## Viewing Traces: A Practical Example + +You can run the included example project to see tracing in action with a local Jaeger instance. + +### Dependencies + +- [Docker](https://docs.docker.com/get-docker/) & [Docker Compose](https://docs.docker.com/compose/install/) +- [AWS Lambda Test Tool](https://github.com/aws/aws-lambda-dotnet/tree/master/Tools/LambdaTestTool-v2) + +### 1. Start Jaeger + +Navigate to the example directory and start the Jaeger container. -### When to Use OpenTelemetry +```bash +cd ./examples/AwsLambda.Host.Example.OpenTelemetry +docker compose up +``` + +Jaeger UI will be available at [`http://localhost:16686`](http://localhost:16686). -Use the OpenTelemetry package when: +### 2. Run the Lambda Function -- ✅ You need distributed tracing across microservices -- ✅ You want to monitor Lambda performance and cold starts -- ✅ You need to export metrics to observability platforms (Datadog, New Relic, etc.) -- ✅ You want automatic instrumentation for Lambda invocations -- ✅ You're building complex serverless architectures +In a new terminal, run the Lambda function. It is configured to export traces to the Jaeger instance started above. + +```bash +# In ./examples/AwsLambda.Host.Example.OpenTelemetry +dotnet run +``` + +### 3. Invoke the Function + +You can use the AWS Lambda Test Tool to invoke the function locally. If you don't have it running, start it from the example directory: + +```bash +dotnet lambda-test-tool +``` + +In the Test Tool UI: +1. Select the `AwsLambda.Host.Example.OpenTelemetry` function. +2. Choose the saved "SayHello" example request from the "Example Requests" dropdown. +3. Click **"Execute"**. + +### 4. View the Trace + +Refresh the Jaeger UI. You should see a new trace for the `AwsLambda.Host.Example.OpenTelemetry` service. Clicking on it will reveal the full trace, including the `OnInit` span and the handler invocation span. + +!!! warning "Traces Not Appearing?" + It may take a few seconds for traces to be exported. If they don't appear, stop the running Lambda function (`Ctrl+C`). This triggers the `OnShutdown` hook, which forces a flush of any buffered telemetry. --- ## Installation -### OpenTelemetry Package +Install the OpenTelemetry integration package and any required exporter packages. ```bash +# Core integration package dotnet add package AwsLambda.Host.OpenTelemetry + +# Common packages for OTLP export +dotnet add package OpenTelemetry.Exporter.Otlp +dotnet add package OpenTelemetry.Extensions.Hosting + +# Packages for X-Ray integration +dotnet add package OpenTelemetry.Contrib.Extensions.AWSXRay +dotnet add package OpenTelemetry.Contrib.Instrumentation.AWS ``` \ No newline at end of file From d89f7a9dd35fbcad383acbafe9d61025023e2a7d Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 17:15:11 -0500 Subject: [PATCH 22/65] chore(docs): refine OpenTelemetry guide with expanded examples and detailed setup - Updated OpenTelemetry integration documentation to include enhanced lifecycle examples. - Added explanations about compile-time instrumentation and shutdown trace flushing. - Expanded configuration sections for OTLP, X-Ray, and custom instrumentation. - Improved example code for better readability and alignment with best practices. - Included guidance for viewing traces using Jaeger and basic CURL commands. --- docs/features/open_telemetry.md | 170 +++++++++++++++----------------- 1 file changed, 80 insertions(+), 90 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 8d3e1b67..5aa54455 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -1,76 +1,106 @@ # OpenTelemetry -**What is OpenTelemetry Integration?** +## What is OpenTelemetry Integration? -The `AwsLambda.Host.OpenTelemetry` package provides comprehensive observability for your serverless application by integrating distributed tracing and metrics collection directly into the AWS Lambda lifecycle. It builds on the vendor-neutral OpenTelemetry SDK, allowing you to export telemetry data to any compatible observability backend. +The `AwsLambda.Host.OpenTelemetry` package provides seamless integration with the official [OpenTelemetry.Instrumentation.AWSLambda](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AWSLambda) package. It acts as a smart adapter layer for `AwsLambda.Host`, using **C# 12 interceptors and source generation** to automatically instrument your Lambda handlers with minimal overhead. + +At compile time, it wraps your handler invocation in a root trace span, providing reflection-free, high-performance distributed tracing. --- ## Key Benefits -- **Automatic Instrumentation**: Automatically creates trace spans for the entire Lambda lifecycle, including `OnInit` (cold starts), handler invocation, and `OnShutdown`. +- **Zero-Overhead Instrumentation**: Uses source generation to create trace spans for your handler at compile time, avoiding runtime reflection. - **Distributed Tracing**: Propagates trace context across services, giving you end-to-end visibility in a microservices architecture. -- **Metrics Collection**: Capture and export performance and business metrics. -- **Lifecycle Integration**: Seamlessly hooks into the `AwsLambda.Host` lifecycle for accurate cold start detection and telemetry flushing. -- **Vendor-Neutral**: Compatible with any OpenTelemetry-compliant backend, including Jaeger, Datadog, New Relic, Honeycomb, and AWS X-Ray. -- **Custom Instrumentation**: Easily create custom spans and metrics to capture application-specific logic. +- **Lifecycle Integration**: Provides an explicit shutdown hook to ensure all buffered telemetry is flushed before the Lambda execution environment is frozen. +- **Vendor-Neutral**: Fully compatible with any OpenTelemetry-compliant backend, including Jaeger, Datadog, New Relic, Honeycomb, and AWS X-Ray. +- **Standard APIs**: Leverages the standard OpenTelemetry .NET SDK, so you can use familiar configuration and custom instrumentation APIs. +- **Custom Instrumentation**: Easily create custom spans (`Activity`) and metrics (`Meter`) to capture application-specific logic. --- ## Quick Start -This example demonstrates how to add basic OpenTelemetry instrumentation. With just a few lines of code, you get automatic tracing for your Lambda handler. +This example demonstrates how to add basic OpenTelemetry instrumentation to your Lambda function. ```csharp title="Program.cs" linenums="1" -using System.Diagnostics; using AwsLambda.Host.Builder; -using AwsLambda.Host.OpenTelemetry; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Instrumentation.AWSLambda; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; var builder = LambdaApplication.CreateBuilder(); -// 1. Add and configure OpenTelemetry -builder.Services.AddOpenTelemetry() - .WithTracing(tracing => tracing - .AddSource("MyCustomSource") // Add a custom source for tracing - .AddConsoleExporter()); // Export traces to the console for debugging +// 1. Add OpenTelemetry tracing to the DI container +builder + .Services.AddOpenTelemetry() + .WithTracing(tracing => + { + // Enable AWS Lambda configurations + tracing.AddAWSLambdaConfigurations(); + // Add a custom source for tracing + tracing.AddSource("MyService"); + tracing.SetResourceBuilder( + ResourceBuilder.CreateDefault().AddService("MyService", serviceVersion: "1.0.0") + ); + // Export traces to the console for debugging + tracing.AddConsoleExporter(); + }); -// 2. Add the Lambda-specific OpenTelemetry instrumentation -builder.Services.AddLambdaOpenTelemetry(); +await using var lambda = builder.Build(); -var lambda = builder.Build(); +// 2. Enable OpenTelemetry tracing in the Lambda host through middleware. +lambda.UseOpenTelemetryTracing(); -// 3. The handler invocation will be automatically traced -lambda.MapHandler(([Event] Request request, ILogger logger) => -{ - // 4. (Optional) Create a custom activity span - using var activity = new ActivitySource("MyCustomSource").StartActivity("ProcessingRequest"); - activity?.SetTag("name", request.Name); +// 3. (Optional) Flush the OpenTelemetry traces at the end of the Lambda execution. +lambda.OnShutdownFlushOpenTelemetry(); - logger.LogInformation("Responding to {Name}", request.Name); - return new Response($"Hello {request.Name}!"); -}); +// 4. Write your Lambda handler like normal. +lambda.MapHandler( + async ([Event] Request request, ILogger logger) => + { + logger.LogInformation("Responding to {Name}", request.Name); + + await Task.Delay(100); // Simulate work + + return new Response($"Hello {request.Name}!"); + } +); await lambda.RunAsync(); internal record Request(string Name); + internal record Response(string Message); ``` --- +## How It Works: Compile-Time Magic + +The `UseOpenTelemetryTracing()` method is the key to this integration. While it looks like a simple method call, it's actually an empty stub that acts as a hook for the `AwsLambda.Host` source generator. + +At compile time, the generator finds this call and **intercepts** it. It then generates code that wraps your handler delegate inside the root tracing logic provided by `OpenTelemetry.Instrumentation.AWSLambda`. This means your handler is automatically timed and traced with no runtime performance penalty from reflection. + +The `OnShutdownFlushOpenTelemetry()` method registers a lifecycle hook that gracefully flushes the OpenTelemetry `TracerProvider` and `MeterProvider`, ensuring all buffered telemetry is sent before the Lambda terminates. + +--- + ## Configuration -Configuration is done using the standard OpenTelemetry .NET SDK extension methods on `IServiceCollection`. The `AddLambdaOpenTelemetry()` call then wires up the instrumentation with the Lambda lifecycle. +Configuration is done using the standard OpenTelemetry .NET SDK extension methods on `IServiceCollection`. ### Basic Configuration (OTLP Exporter) The most common scenario is exporting telemetry to an observability platform via the OpenTelemetry Protocol (OTLP). ```csharp title="Program.cs" linenums="1" -var builder = LambdaApplication.CreateBuilder(); +var lambda = LambdaApplication.Create(); -builder.Services.AddOpenTelemetry() +lambda.Services.AddOpenTelemetry() .WithTracing(tracing => tracing .AddAWSLambdaConfigurations() // Adds Lambda-specific resource attributes .AddOtlpExporter()) // Exports traces via OTLP @@ -78,58 +108,27 @@ builder.Services.AddOpenTelemetry() .AddAWSLambdaConfigurations() // Adds Lambda-specific resource attributes .AddOtlpExporter()); // Exports metrics via OTLP -builder.Services.AddLambdaOpenTelemetry(); +lambda.UseOpenTelemetryTracing(); +lambda.OnShutdownFlushOpenTelemetry(); -var lambda = builder.Build(); +var app = lambda.Build(); // ... ``` !!! tip "OTLP Endpoint Configuration" The OTLP exporter endpoint is configured via the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. -### Configuring Tracing - -You can configure tracing providers, add custom sources, and define sampling rules. - -```csharp title="Program.cs" linenums="1" -using OpenTelemetry.Trace; - -// ... - -builder.Services.AddOpenTelemetry() - .WithTracing(tracing => tracing - .AddAWSLambdaConfigurations() - .AddSource("MyApplication.*") // Listen to custom ActivitySources - .SetSampler(new AlwaysOnSampler()) // Configure sampling - .AddOtlpExporter()); -``` - -### Configuring Metrics - -Configure metrics providers and add custom meters to record application-specific measurements. - -```csharp title="Program.cs" linenums="1" -using OpenTelemetry.Metrics; - -// ... - -builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => metrics - .AddAWSLambdaConfigurations() - .AddMeter("MyApplication.Metrics") // Listen to custom Meters - .AddPrometheusExporter()); // Or any other exporter -``` - ### AWS X-Ray Integration To integrate with AWS X-Ray, use the AWS-provided instrumentation and propagator. ```csharp title="Program.cs" linenums="1" using OpenTelemetry.Trace; +using OpenTelemetry.Contrib.Extensions.AWSXRay; // AWS X-Ray Propagator // ... -builder.Services.AddOpenTelemetry() +lambda.Services.AddOpenTelemetry() .WithTracing(tracing => { tracing @@ -140,19 +139,11 @@ builder.Services.AddOpenTelemetry() // Propagate X-Ray context Sdk.SetDefaultTextMapPropagator(new AWSXRayPropagator()); }); -``` - ---- - -## Lifecycle Integration - -`AwsLambda.Host.OpenTelemetry` automatically instruments the key phases of the Lambda execution lifecycle. - -- **`OnInit`**: The application startup phase (`OnInitAsync`) is wrapped in its own trace span. This allows you to precisely measure cold start times by analyzing the duration of the `OnInit` span. -- **Invocation**: Each handler invocation is automatically traced. The trace captures the full execution time, including any middleware in the pipeline. - -- **`OnShutdown`**: The `OnShutdownAsync` phase is traced. Importantly, the OpenTelemetry `TracerProvider` is flushed during this phase, ensuring that all buffered telemetry is sent before the execution environment is frozen. +lambda.UseOpenTelemetryTracing(); +lambda.OnShutdownFlushOpenTelemetry(); +// ... +``` --- @@ -218,7 +209,6 @@ You can run the included example project to see tracing in action with a local J ### Dependencies - [Docker](https://docs.docker.com/get-docker/) & [Docker Compose](https://docs.docker.com/compose/install/) -- [AWS Lambda Test Tool](https://github.com/aws/aws-lambda-dotnet/tree/master/Tools/LambdaTestTool-v2) ### 1. Start Jaeger @@ -242,23 +232,21 @@ dotnet run ### 3. Invoke the Function -You can use the AWS Lambda Test Tool to invoke the function locally. If you don't have it running, start it from the example directory: +You can use any tool to send a POST request to `http://localhost:8080`, which is the default for `dotnet run`. +Using `curl`: ```bash -dotnet lambda-test-tool +curl -X POST "http://localhost:8080" \ +-H "Content-Type: application/json" \ +-d '{"Name": "World"}' ``` -In the Test Tool UI: -1. Select the `AwsLambda.Host.Example.OpenTelemetry` function. -2. Choose the saved "SayHello" example request from the "Example Requests" dropdown. -3. Click **"Execute"**. - ### 4. View the Trace -Refresh the Jaeger UI. You should see a new trace for the `AwsLambda.Host.Example.OpenTelemetry` service. Clicking on it will reveal the full trace, including the `OnInit` span and the handler invocation span. +Refresh the Jaeger UI. You should see a new trace for the service. Clicking on it will reveal the full trace, including the handler invocation span and any custom spans you created. !!! warning "Traces Not Appearing?" - It may take a few seconds for traces to be exported. If they don't appear, stop the running Lambda function (`Ctrl+C`). This triggers the `OnShutdown` hook, which forces a flush of any buffered telemetry. + It may take a few seconds for traces to be exported. If they don't appear, stop the running Lambda function (`Ctrl+C`). This triggers the shutdown hook, which forces a flush of any buffered telemetry. --- @@ -270,11 +258,13 @@ Install the OpenTelemetry integration package and any required exporter packages # Core integration package dotnet add package AwsLambda.Host.OpenTelemetry +# Official AWS Lambda Instrumentation +dotnet add package OpenTelemetry.Instrumentation.AWSLambda + # Common packages for OTLP export dotnet add package OpenTelemetry.Exporter.Otlp dotnet add package OpenTelemetry.Extensions.Hosting # Packages for X-Ray integration dotnet add package OpenTelemetry.Contrib.Extensions.AWSXRay -dotnet add package OpenTelemetry.Contrib.Instrumentation.AWS -``` \ No newline at end of file +``` From 7ffa72dc8329e802a6d12cb374710df8c8bab01c Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 17:17:23 -0500 Subject: [PATCH 23/65] chore(docs): improve OpenTelemetry quick start with installation and updated examples - Added detailed installation instructions for required packages, including OTLP and X-Ray exporters. - Updated example code to include `CancellationToken` for better asynchronous handling. - Reorganized content to enhance readability and follow a logical progression. --- docs/features/open_telemetry.md | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 5aa54455..e5d911f5 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -21,6 +21,27 @@ At compile time, it wraps your handler invocation in a root trace span, providin ## Quick Start +### Installation + +Install the OpenTelemetry integration package and any required exporter packages. + +```bash +# Core integration package +dotnet add package AwsLambda.Host.OpenTelemetry + +# Official AWS Lambda Instrumentation +dotnet add package OpenTelemetry.Instrumentation.AWSLambda + +# Common packages for OTLP export +dotnet add package OpenTelemetry.Exporter.Otlp +dotnet add package OpenTelemetry.Extensions.Hosting + +# Packages for X-Ray integration +dotnet add package OpenTelemetry.Contrib.Extensions.AWSXRay +``` + +### MVP Code Example + This example demonstrates how to add basic OpenTelemetry instrumentation to your Lambda function. ```csharp title="Program.cs" linenums="1" @@ -60,11 +81,11 @@ lambda.OnShutdownFlushOpenTelemetry(); // 4. Write your Lambda handler like normal. lambda.MapHandler( - async ([Event] Request request, ILogger logger) => + async ([Event] Request request, ILogger logger, CancellationToken cancellationToken) => { logger.LogInformation("Responding to {Name}", request.Name); - await Task.Delay(100); // Simulate work + await Task.Delay(100, cancellationToken); // Simulate work return new Response($"Hello {request.Name}!"); } @@ -247,24 +268,3 @@ Refresh the Jaeger UI. You should see a new trace for the service. Clicking on i !!! warning "Traces Not Appearing?" It may take a few seconds for traces to be exported. If they don't appear, stop the running Lambda function (`Ctrl+C`). This triggers the shutdown hook, which forces a flush of any buffered telemetry. - ---- - -## Installation - -Install the OpenTelemetry integration package and any required exporter packages. - -```bash -# Core integration package -dotnet add package AwsLambda.Host.OpenTelemetry - -# Official AWS Lambda Instrumentation -dotnet add package OpenTelemetry.Instrumentation.AWSLambda - -# Common packages for OTLP export -dotnet add package OpenTelemetry.Exporter.Otlp -dotnet add package OpenTelemetry.Extensions.Hosting - -# Packages for X-Ray integration -dotnet add package OpenTelemetry.Contrib.Extensions.AWSXRay -``` From 7a53b2ba345536ff16b52d4c2a8e3bb7b6a7f032 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 19:36:43 -0500 Subject: [PATCH 24/65] chore(docs): update OpenTelemetry guide with clarified examples and additional tips - Added inline comments in the code examples for better understanding of tracing configurations. - Included a section summarizing key setup steps with numbered annotations. - Updated mkdocs.yml to include the new `content.code.annotate` plugin for improved documentation. --- AwsLambda.Host.sln | 1 - docs/features/open_telemetry.md | 34 +++++++++++++++++++-------------- mkdocs.yml | 2 +- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/AwsLambda.Host.sln b/AwsLambda.Host.sln index 4b52afef..d0bf4f62 100644 --- a/AwsLambda.Host.sln +++ b/AwsLambda.Host.sln @@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CLAUDE.md = CLAUDE.md CLAUDE.local.md = CLAUDE.local.md Directory.Packages.props = Directory.Packages.props - tasks\FormattingTasks.yml = tasks\FormattingTasks.yml THIRD-PARTY-LICENSES.txt = THIRD-PARTY-LICENSES.txt mkdocs.yml = mkdocs.yml EndProjectSection diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index e5d911f5..29d3b629 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -55,32 +55,25 @@ using OpenTelemetry.Trace; var builder = LambdaApplication.CreateBuilder(); -// 1. Add OpenTelemetry tracing to the DI container builder - .Services.AddOpenTelemetry() + .Services.AddOpenTelemetry()// (1)! .WithTracing(tracing => { - // Enable AWS Lambda configurations - tracing.AddAWSLambdaConfigurations(); - // Add a custom source for tracing - tracing.AddSource("MyService"); + tracing.AddAWSLambdaConfigurations();// (2)! + tracing.AddSource("MyService");// (3)! tracing.SetResourceBuilder( ResourceBuilder.CreateDefault().AddService("MyService", serviceVersion: "1.0.0") ); - // Export traces to the console for debugging - tracing.AddConsoleExporter(); + tracing.AddConsoleExporter();// (4)! }); await using var lambda = builder.Build(); -// 2. Enable OpenTelemetry tracing in the Lambda host through middleware. -lambda.UseOpenTelemetryTracing(); +lambda.UseOpenTelemetryTracing();// (5)! -// 3. (Optional) Flush the OpenTelemetry traces at the end of the Lambda execution. -lambda.OnShutdownFlushOpenTelemetry(); +lambda.OnShutdownFlushOpenTelemetry();// (6)! -// 4. Write your Lambda handler like normal. -lambda.MapHandler( +lambda.MapHandler(// (7)! async ([Event] Request request, ILogger logger, CancellationToken cancellationToken) => { logger.LogInformation("Responding to {Name}", request.Name); @@ -98,6 +91,19 @@ internal record Request(string Name); internal record Response(string Message); ``` +1. Add OpenTelemetry tracing to the DI container +2. Enable AWS Lambda configurations +3. Add a custom source for tracing +4. Export traces to a collector, in this case the console +5. Enable OpenTelemetry tracing in the Lambda host through middleware. +6. (Optional) Flush the OpenTelemetry traces at the end of the Lambda execution. +7. Write your Lambda handler like normal. + +!!! tip + OpenTelemetry tracing can be configured in multiple ways, including manually creating a trace provider using the [OpenTelemetry](https://www.nuget.org/packages/OpenTelemetry), or through registering OpenTelemetry services in your DI container using [OpenTelemetry.Extensions.Hosting](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting). + + When working with `AwsLambda.Host`, its recommended to the latter approach. + --- ## How It Works: Compile-Time Magic diff --git a/mkdocs.yml b/mkdocs.yml index 07312f22..5c029ec6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,7 +29,7 @@ theme: - search.highlight - search.share - search.suggest - + - content.code.annotate palette: # Palette toggle for light mode From dafb7f4469ede2b05df5de3e902ffb030257cf0a Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 20:18:04 -0500 Subject: [PATCH 25/65] chore(docs): update OpenTelemetry guide with new source generation details and package corrections - Added explanation of source generation and interceptors for performance optimization. - Replaced outdated `OpenTelemetry.Exporter.Otlp` package with `OpenTelemetry.Exporter.OpenTelemetryProtocol`. - Enhanced clarity in the "How It Works" section, focusing on optimized instrumentation flow. - Simplified configuration examples by removing excessive code while maintaining key details. - Updated `mkdocs.yml` to include `def_list` and `pymdownx.tasklist` Markdown extensions. --- docs/features/open_telemetry.md | 81 ++++++++------------------------- mkdocs.yml | 3 ++ 2 files changed, 21 insertions(+), 63 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 29d3b629..c59d53da 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -29,11 +29,8 @@ Install the OpenTelemetry integration package and any required exporter packages # Core integration package dotnet add package AwsLambda.Host.OpenTelemetry -# Official AWS Lambda Instrumentation -dotnet add package OpenTelemetry.Instrumentation.AWSLambda - # Common packages for OTLP export -dotnet add package OpenTelemetry.Exporter.Otlp +dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol dotnet add package OpenTelemetry.Extensions.Hosting # Packages for X-Ray integration @@ -102,81 +99,39 @@ internal record Response(string Message); !!! tip OpenTelemetry tracing can be configured in multiple ways, including manually creating a trace provider using the [OpenTelemetry](https://www.nuget.org/packages/OpenTelemetry), or through registering OpenTelemetry services in your DI container using [OpenTelemetry.Extensions.Hosting](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting). - When working with `AwsLambda.Host`, its recommended to the latter approach. - ---- - -## How It Works: Compile-Time Magic - -The `UseOpenTelemetryTracing()` method is the key to this integration. While it looks like a simple method call, it's actually an empty stub that acts as a hook for the `AwsLambda.Host` source generator. - -At compile time, the generator finds this call and **intercepts** it. It then generates code that wraps your handler delegate inside the root tracing logic provided by `OpenTelemetry.Instrumentation.AWSLambda`. This means your handler is automatically timed and traced with no runtime performance penalty from reflection. - -The `OnShutdownFlushOpenTelemetry()` method registers a lifecycle hook that gracefully flushes the OpenTelemetry `TracerProvider` and `MeterProvider`, ensuring all buffered telemetry is sent before the Lambda terminates. + When working with `AwsLambda.Host`, its recommended to the latter approach and as such, this documentation focuses on it. --- -## Configuration - -Configuration is done using the standard OpenTelemetry .NET SDK extension methods on `IServiceCollection`. - -### Basic Configuration (OTLP Exporter) - -The most common scenario is exporting telemetry to an observability platform via the OpenTelemetry Protocol (OTLP). - -```csharp title="Program.cs" linenums="1" -var lambda = LambdaApplication.Create(); - -lambda.Services.AddOpenTelemetry() - .WithTracing(tracing => tracing - .AddAWSLambdaConfigurations() // Adds Lambda-specific resource attributes - .AddOtlpExporter()) // Exports traces via OTLP - .WithMetrics(metrics => metrics - .AddAWSLambdaConfigurations() // Adds Lambda-specific resource attributes - .AddOtlpExporter()); // Exports metrics via OTLP +## How It Works: Source Generation and Interceptors -lambda.UseOpenTelemetryTracing(); -lambda.OnShutdownFlushOpenTelemetry(); +`AwsLambda.Host` relies on C# source generators to avoid runtime reflection and improve performance. The `UseOpenTelemetryTracing()` method is a key part of this system. -var app = lambda.Build(); -// ... -``` +Here's the step-by-step process: -!!! tip "OTLP Endpoint Configuration" - The OTLP exporter endpoint is configured via the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. +1. The `UseOpenTelemetryTracing()` method itself is an empty placeholder. +2. At compile time, a source generator finds all calls to this method. +3. For each call it finds, it emits a C# 12 Interceptor. This interceptor replaces the empty method call with code that adds a middleware delegate to the Lambda invocation pipeline. +4. This generated middleware calls an adapter in the `AwsLambda.Host.OpenTelemetry` package, which in turn uses the official `OpenTelemetry.Instrumentation.AWSLambda` package to wrap the Lambda handler execution in a root trace span. -### AWS X-Ray Integration +This entire process happens during compilation, resulting in highly optimized code that instruments your handler without any reflection overhead at runtime. -To integrate with AWS X-Ray, use the AWS-provided instrumentation and propagator. +Similarly, `OnShutdownFlushOpenTelemetry()` is an interceptor that registers a shutdown hook. This hook flushes the OpenTelemetry providers, ensuring buffered telemetry is sent before the Lambda execution environment terminates. -```csharp title="Program.cs" linenums="1" -using OpenTelemetry.Trace; -using OpenTelemetry.Contrib.Extensions.AWSXRay; // AWS X-Ray Propagator - -// ... - -lambda.Services.AddOpenTelemetry() - .WithTracing(tracing => - { - tracing - .AddAWSLambdaConfigurations() - .AddAWSXRayTraceId() // Use X-Ray format for Trace IDs - .AddOtlpExporter(); +--- - // Propagate X-Ray context - Sdk.SetDefaultTextMapPropagator(new AWSXRayPropagator()); - }); +## Configuration -lambda.UseOpenTelemetryTracing(); -lambda.OnShutdownFlushOpenTelemetry(); -// ... -``` +Configuration is done using the standard OpenTelemetry .NET SDK extension methods on `IServiceCollection`. Official documentation for these methods can be found [here](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Extensions.Hosting/README.md). --- ## Custom Instrumentation -To get the most out of observability, you should add custom instrumentation to your application code. +`AwsLambda.Host.OpenTelemetry` helps you instrement your Lambda handlers with [OpenTelemetry.Instrumentation.AWSLambda](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AWSLambda), but to get the most out of observability, you should add custom instrumentation to your application code. In this section we cover how this can be done. + +!!! note + This code is not specific to `AwsLambda.Host.OpenTelemetry` and follows the guidlines provided by Microsoft's [.NET distributed tracing documetation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). ### Custom Spans (Activities) diff --git a/mkdocs.yml b/mkdocs.yml index 5c029ec6..6f97c701 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,9 @@ markdown_extensions: alternate_style: true - pymdownx.details - admonition + - def_list + - pymdownx.tasklist: + custom_checkbox: true watch: - ./docs/ From 566c099f783332e8f490a6f89682f3c28761249d Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 20:35:09 -0500 Subject: [PATCH 26/65] chore(docs): expand OpenTelemetry guide with examples for metrics and instrumentation - Added sections on custom metrics instrumentation with `Counter` and `Metric` examples. - Expanded documentation to include handler and service instrumentation with detailed code samples. - Reorganized headings for improved readability and logical flow. - Included references to working example project for better hands-on understanding. --- docs/features/open_telemetry.md | 134 +++++++++++++++++--------------- 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index c59d53da..3f9af8a0 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -120,112 +120,118 @@ Similarly, `OnShutdownFlushOpenTelemetry()` is an interceptor that registers a s --- -## Configuration +## Working With `AwsLambda.Host.OpenTelemetry` + +### Configuration Configuration is done using the standard OpenTelemetry .NET SDK extension methods on `IServiceCollection`. Official documentation for these methods can be found [here](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Extensions.Hosting/README.md). +### Instrumenting The Invocation Pipeline + +### Gracefully Shutting & Cleaning Up + --- -## Custom Instrumentation +## Manual Instrumentation -`AwsLambda.Host.OpenTelemetry` helps you instrement your Lambda handlers with [OpenTelemetry.Instrumentation.AWSLambda](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AWSLambda), but to get the most out of observability, you should add custom instrumentation to your application code. In this section we cover how this can be done. +`AwsLambda.Host.OpenTelemetry` helps you instrement your Lambda handlers with [OpenTelemetry.Instrumentation.AWSLambda](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AWSLambda), but to get the most out of observability, you should add custom instrumentation to your application code. In this section we cover how this can be done easily with the Dependancy Injection support provided by `AwsLambda.Host`. !!! note This code is not specific to `AwsLambda.Host.OpenTelemetry` and follows the guidlines provided by Microsoft's [.NET distributed tracing documetation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). -### Custom Spans (Activities) +A full working example of an instrumented Lambda application can be found in [here](../../examples/AwsLambda.Host.Example.OpenTelemetry) -Inject an `ActivitySource` to create custom spans that represent specific units of work, like a database call or an API request. +### Custom Instrumentation Class -```csharp title="NameService.cs" linenums="1" +```csharp title="Instrumentation.cs" linenums="1" using System.Diagnostics; -public class NameService -{ - private static readonly ActivitySource Source = new("MyApplication.NameService"); +namespace AwsLambda.Host.Example.OpenTelemetry; - public string GetFullName(string name) - { - using var activity = Source.StartActivity("GetFullName"); - activity?.SetTag("input.name", name); - - // ... some work ... +/// +/// It is recommended to use a custom type to hold references for ActivitySource. This avoids +/// possible type collisions with other components in the DI container. +/// +internal class Instrumentation : IDisposable +{ + internal const string ActivitySourceName = "MyLambda"; + internal const string ActivitySourceVersion = "1.0.0"; - var fullName = $"{name} Smith"; - activity?.SetTag("output.fullname", fullName); + internal ActivitySource ActivitySource { get; } = + new(ActivitySourceName, ActivitySourceVersion); - return fullName; - } + public void Dispose() => ActivitySource.Dispose(); } ``` -### Custom Metrics - -Inject a `Meter` and create `Counter`, `Histogram`, or `UpDownCounter` instruments to record business or performance metrics. +### Custom Metrics Class -```csharp title="OrderProcessor.cs" linenums="1" +```csharp title="NameMetrics.cs" linenums="1" using System.Diagnostics.Metrics; -public class OrderProcessor +namespace AwsLambda.Host.Example.OpenTelemetry; + +public class NameMetrics { - private static readonly Meter Meter = new("MyApplication.Metrics"); - private static readonly Counter OrdersProcessed = Meter.CreateCounter("orders.processed"); - private static readonly Histogram OrderValue = Meter.CreateHistogram("orders.value"); + private readonly Counter _namesProcessed; - public void Process(Order order) + public NameMetrics(IMeterFactory meterFactory) { - // ... process order ... - - OrdersProcessed.Add(1); - OrderValue.Record(order.Value); + var meter = meterFactory.Create("MyLambda.Service"); + _namesProcessed = meter.CreateCounter("MyLambda.Service.Processed"); } + + public void ProcessName(string name) => + _namesProcessed.Add(1, new KeyValuePair("name", name)); } ``` ---- -## Viewing Traces: A Practical Example +### Instrument A Service -You can run the included example project to see tracing in action with a local Jaeger instance. +```csharp title="NameService.cs" linenums="1" +using System.Diagnostics; -### Dependencies +public class NameService +{ + private static readonly ActivitySource Source = new("MyApplication.NameService"); -- [Docker](https://docs.docker.com/get-docker/) & [Docker Compose](https://docs.docker.com/compose/install/) + public string GetFullName(string name) + { + using var activity = Source.StartActivity("GetFullName"); + activity?.SetTag("input.name", name); -### 1. Start Jaeger + // ... some work ... -Navigate to the example directory and start the Jaeger container. + var fullName = $"{name} Smith"; + activity?.SetTag("output.fullname", fullName); -```bash -cd ./examples/AwsLambda.Host.Example.OpenTelemetry -docker compose up + return fullName; + } +} ``` -Jaeger UI will be available at [`http://localhost:16686`](http://localhost:16686). +### Instrument A Handler -### 2. Run the Lambda Function - -In a new terminal, run the Lambda function. It is configured to export traces to the Jaeger instance started above. +```csharp title="Function.cs" linenums="1" +using AwsLambda.Host.Builder; -```bash -# In ./examples/AwsLambda.Host.Example.OpenTelemetry -dotnet run -``` +namespace AwsLambda.Host.Example.OpenTelemetry; -### 3. Invoke the Function +internal static class Function +{ + internal static async Task Handler( + [Event] Request request, + IService service, + Instrumentation instrumentation, + CancellationToken cancellationToken + ) + { + using var activity = instrumentation.ActivitySource.StartActivity(); -You can use any tool to send a POST request to `http://localhost:8080`, which is the default for `dotnet run`. + var message = await service.GetMessage(request.Name, cancellationToken); -Using `curl`: -```bash -curl -X POST "http://localhost:8080" \ --H "Content-Type: application/json" \ --d '{"Name": "World"}' + return new Response(message, DateTime.UtcNow); + } +} ``` - -### 4. View the Trace - -Refresh the Jaeger UI. You should see a new trace for the service. Clicking on it will reveal the full trace, including the handler invocation span and any custom spans you created. - -!!! warning "Traces Not Appearing?" - It may take a few seconds for traces to be exported. If they don't appear, stop the running Lambda function (`Ctrl+C`). This triggers the shutdown hook, which forces a flush of any buffered telemetry. From c02150c66a7c16c78e04c7ee02a2205c56b1fbd1 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 20:45:24 -0500 Subject: [PATCH 27/65] chore(docs): update OpenTelemetry guide with DI provider expectations and shutdown tips - Added notes about `TracerProvider` requirements in the DI container for proper functionality. - Included warnings and tips on trace flushing during application shutdown. - Enhanced documentation with Lambda execution environment lifecycle insights. - Fixed typos and improved readability in affected sections. --- docs/features/open_telemetry.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 3f9af8a0..0de238a2 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -2,7 +2,7 @@ ## What is OpenTelemetry Integration? -The `AwsLambda.Host.OpenTelemetry` package provides seamless integration with the official [OpenTelemetry.Instrumentation.AWSLambda](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AWSLambda) package. It acts as a smart adapter layer for `AwsLambda.Host`, using **C# 12 interceptors and source generation** to automatically instrument your Lambda handlers with minimal overhead. +The `AwsLambda.Host.OpenTelemetry` package provides seamless integration with the official [`OpenTelemetry.Instrumentation.AWSLambda`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AWSLambda) package. It acts as a smart adapter layer for `AwsLambda.Host`, using **C# 12 interceptors and source generation** to automatically instrument your Lambda handlers with minimal overhead. At compile time, it wraps your handler invocation in a root trace span, providing reflection-free, high-performance distributed tracing. @@ -126,10 +126,18 @@ Similarly, `OnShutdownFlushOpenTelemetry()` is an interceptor that registers a s Configuration is done using the standard OpenTelemetry .NET SDK extension methods on `IServiceCollection`. Official documentation for these methods can be found [here](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Extensions.Hosting/README.md). +`AwsLambda.Host.OpenTelemetry` expectes for an instance of `TracerProvider` to be registered in the DI container as this provider is used by `OpenTelemetry.Instrumentation.AWSLambda`. As such, it is your responsibility to configure the OpenTelemetry provider and ensure it is registered in the DI container. If it is not, an exception will be thrown at startup. + ### Instrumenting The Invocation Pipeline + ### Gracefully Shutting & Cleaning Up +Since it is expected that the trace provider is added to the DI container and since trace provider implements IDisposable, when the application shuts down and the dependency injection container is disposed of, the trace provider should automatically flush all of its data. If you experience issues where traces are not being properly flushed on shutdown and you are seeing traces dropped or not properly propagated, `AwsLambda.Host.OpenTelemetry` prvides a couple of helper methods: + +!!! Warning + When shutting down, Lambda only allocated up to 500ms of time for the execution environment to shut down. As such, it is important to make sure that shutdown logic such as flushing traces is executed as quickly as possible. More information about the Lambda execution environment lifecycle can be found [here](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html). + --- ## Manual Instrumentation From 3e222fc2669e7ef2e9ead592a24ab15cf1db58f2 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 20:57:09 -0500 Subject: [PATCH 28/65] chore(docs): extend OpenTelemetry guide with shutdown flush utilities for Lambda - Documented new helper methods: `OnShutdownFlushOpenTelemetry`, `OnShutdownFlushTracer`, and `OnShutdownFlushMeter`. - Added detailed descriptions of usage scenarios and parameters, including `timeoutMilliseconds`. - Clarified behavior of `TracerProvider` and `MeterProvider` during shutdown in serverless environments. - Enhanced warnings about Lambda lifecycle constraints during shutdown. - Fixed minor typos and improved Markdown formatting. --- docs/features/open_telemetry.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 0de238a2..b0d6f656 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -133,10 +133,24 @@ Configuration is done using the standard OpenTelemetry .NET SDK extension method ### Gracefully Shutting & Cleaning Up -Since it is expected that the trace provider is added to the DI container and since trace provider implements IDisposable, when the application shuts down and the dependency injection container is disposed of, the trace provider should automatically flush all of its data. If you experience issues where traces are not being properly flushed on shutdown and you are seeing traces dropped or not properly propagated, `AwsLambda.Host.OpenTelemetry` prvides a couple of helper methods: +The OpenTelemetry `TracerProvider` and `MeterProvider` services both implement `IDisposable`. When the dependency injection container is disposed of during a normal application shutdown, it should trigger these providers to automatically flush any buffered telemetry. However, in a serverless environment where the lifecycle can be abrupt, this disposal is not always guaranteed to complete before the execution environment is frozen. + +For situations where you notice data being dropped, or if you want to guarantee a flush attempt is made, `AwsLambda.Host.OpenTelemetry` provides the following explicit helper methods. They register a function during the application's shutdown phase to manually force-flush pending telemetry. + +The following methods are available to be called on the `LambdaApplication` instance: + +| Method | Description | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `OnShutdownFlushOpenTelemetry()` | A convenience method that registers shutdown hooks to flush both traces and metrics. It calls both `OnShutdownFlushTracer` and `OnShutdownFlushMeter` internally. This is the recommended method for most users. | +| `OnShutdownFlushTracer()` | Registers a shutdown hook to force-flush only the `TracerProvider`. Use this if you are only tracing and not collecting metrics, or if you need separate control over flushing traces. | +| `OnShutdownFlushMeter()` | Registers a shutdown hook to force-flush only the `MeterProvider`. Use this if you are only collecting metrics and not tracing. | + +For most applications, calling `lambda.OnShutdownFlushOpenTelemetry()` is sufficient to ensure all telemetry is flushed. If your application only uses tracing or metrics, but not both, you can use the more specific methods for clarity. + +All three methods also accept an optional `timeoutMilliseconds` parameter. This allows you to specify a maximum duration for the flush operation. Importantly, these flush operations are non-blocking and respect the provided `CancellationToken`, ensuring they can gracefully exit if the Lambda execution environment signals a shutdown before the timeout elapses. This combined approach offers robust control over the flush duration within the limited time available during a Lambda shutdown. !!! Warning - When shutting down, Lambda only allocated up to 500ms of time for the execution environment to shut down. As such, it is important to make sure that shutdown logic such as flushing traces is executed as quickly as possible. More information about the Lambda execution environment lifecycle can be found [here](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html). + When shutting down, Lambda only allocates up to 500ms of time for the execution environment to shut down. As such, it is important to make sure that shutdown logic such as flushing traces is executed as quickly as possible. More information about the Lambda execution environment lifecycle can be found [here](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html). --- From 3e766561b2b700baa26f03796d2a97e9d425d70b Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 20:59:46 -0500 Subject: [PATCH 29/65] chore(docs): add OpenTelemetry tracing middleware usage and source generation details - Documented the `UseOpenTelemetryTracing` extension method for Lambda invocation pipeline. - Explained how the source generator injects middleware and creates root trace spans per invocation. - Provided details on event type inspection and strong typing for OpenTelemetry instrumentation. - Improved documentation readability, including better formatting in method tables. --- docs/features/open_telemetry.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index b0d6f656..ab4af46c 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -128,7 +128,22 @@ Configuration is done using the standard OpenTelemetry .NET SDK extension method `AwsLambda.Host.OpenTelemetry` expectes for an instance of `TracerProvider` to be registered in the DI container as this provider is used by `OpenTelemetry.Instrumentation.AWSLambda`. As such, it is your responsibility to configure the OpenTelemetry provider and ensure it is registered in the DI container. If it is not, an exception will be thrown at startup. -### Instrumenting The Invocation Pipeline +### Instrumenting The Invocation Pipeline + +After configuring the OpenTelemetry services, you need to add the tracing middleware to the Lambda invocation pipeline. This is done by calling the `UseOpenTelemetryTracing()` extension method on the `LambdaApplication` instance. + +```csharp +await using var lambda = builder.Build(); + +// This method call enables the tracing middleware +lambda.UseOpenTelemetryTracing(); + +// ... MapHandler, OnShutdown, etc. ... +``` + +This method call acts as a compile-time trigger for a source generator. The generator intercepts the call and injects middleware into the request pipeline. This middleware is responsible for creating the root trace span for each Lambda invocation. + +Under the covers, the source generator performs a critical task. It inspects the delegate you provided to `MapHandler` to determine the specific input and output types of your function (e.g., `APIGatewayProxyRequest`, `SQSEvent`). It then uses these types to generate a call to a generic helper method. This ensures that the underlying `OpenTelemetry.Instrumentation.AWSLambda` package receives a strongly-typed request object. By preserving the specific event type, the OpenTelemetry instrumentation can correctly extract context and attributes, such as trace parent headers from an API Gateway request, ensuring proper distributed trace propagation. ### Gracefully Shutting & Cleaning Up @@ -139,11 +154,11 @@ For situations where you notice data being dropped, or if you want to guarantee The following methods are available to be called on the `LambdaApplication` instance: -| Method | Description | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `OnShutdownFlushOpenTelemetry()` | A convenience method that registers shutdown hooks to flush both traces and metrics. It calls both `OnShutdownFlushTracer` and `OnShutdownFlushMeter` internally. This is the recommended method for most users. | -| `OnShutdownFlushTracer()` | Registers a shutdown hook to force-flush only the `TracerProvider`. Use this if you are only tracing and not collecting metrics, or if you need separate control over flushing traces. | -| `OnShutdownFlushMeter()` | Registers a shutdown hook to force-flush only the `MeterProvider`. Use this if you are only collecting metrics and not tracing. | +| Method | Description | +|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `OnShutdownFlushOpenTelemetry()` | A convenience method that registers shutdown hooks to flush both traces and metrics. It calls both `OnShutdownFlushTracer` and `OnShutdownFlushMeter` internally. This is the recommended method for most users. | +| `OnShutdownFlushTracer()` | Registers a shutdown hook to force-flush only the `TracerProvider`. Use this if you are only tracing and not collecting metrics, or if you need separate control over flushing traces. | +| `OnShutdownFlushMeter()` | Registers a shutdown hook to force-flush only the `MeterProvider`. Use this if you are only collecting metrics and not tracing. | For most applications, calling `lambda.OnShutdownFlushOpenTelemetry()` is sufficient to ensure all telemetry is flushed. If your application only uses tracing or metrics, but not both, you can use the more specific methods for clarity. From 00ac5be2c00f6cbc295ebc6dc8fa3d719c5dc9d3 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 30 Nov 2025 21:02:07 -0500 Subject: [PATCH 30/65] chore(docs): update example to use `await using` for Lambda lifecycle management - Replaced `var lambda = builder.Build();` with `await using var lambda = builder.Build();`. - Ensured proper asynchronous disposal of `lambda` to improve resource management. --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index c84dda6b..1d997555 100644 --- a/docs/index.md +++ b/docs/index.md @@ -169,7 +169,7 @@ using Microsoft.Extensions.Hosting; var builder = LambdaApplication.CreateBuilder(); // Build the Lambda application -var lambda = builder.Build(); +await using var lambda = builder.Build(); // Map your handler - the event is automatically injected lambda.MapHandler(([Event] string name) => $"Hello {name}!"); From 1394432d9aba54754b5523613164e0740e0f98a3 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 08:55:23 -0500 Subject: [PATCH 31/65] chore(docs): enhance OpenTelemetry guide with detailed instrumentation examples - Added guidance on setting up a dedicated `ActivitySource` for distributed tracing consistency. - Included examples demonstrating custom `Instrumentation` and `Metrics` classes with DI usage. - Documented best practices for handling spans and metrics in services and Lambda handlers. - Emphasized alignment with .NET distributed tracing and instrumentation walkthroughs. - Improved documentation clarity by integrating practical examples and usage scenarios. --- docs/features/open_telemetry.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index ab4af46c..2a11fa91 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -180,6 +180,8 @@ A full working example of an instrumented Lambda application can be found in [he ### Custom Instrumentation Class +The [.NET distributed tracing guidance](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing) recommends creating a dedicated `ActivitySource` per service or bounded context, then sharing it through dependency injection. This keeps source names consistent and avoids collisions when multiple libraries emit spans. + ```csharp title="Instrumentation.cs" linenums="1" using System.Diagnostics; @@ -201,8 +203,12 @@ internal class Instrumentation : IDisposable } ``` +Register this class (typically as a singleton) so that services can request an `Instrumentation` instance and start spans with a stable `ActivitySourceName`. Keeping the type disposable mirrors the official walkthrough for manual instrumentation and ensures underlying event listeners are released when the Lambda host shuts down. + ### Custom Metrics Class +Metrics follow a similar pattern: create a class that receives an `IMeterFactory`, then build strongly typed instruments. This matches the [instrumentation walkthroughs](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs) where meters and counters are grouped by concern. + ```csharp title="NameMetrics.cs" linenums="1" using System.Diagnostics.Metrics; @@ -223,9 +229,12 @@ public class NameMetrics } ``` +By injecting `NameMetrics` into your services you can increment counters, attach semantic tags (in this case the processed `name`), and have the values exported alongside traces through whatever OTLP or X-Ray exporter you registered earlier. ### Instrument A Service +Once the reusable helpers exist, wrap service logic in spans to capture timing, tags, and exceptions. The following `NameService` starts a child activity every time it generates a value, enriching it with both input and output information. + ```csharp title="NameService.cs" linenums="1" using System.Diagnostics; @@ -248,8 +257,12 @@ public class NameService } ``` +The `using var activity = ...` pattern mirrors the BCL samples and guarantees spans finish even when exceptions are thrown. You can call into `NameMetrics.ProcessName` inside the same scope so traces and metrics share correlated attributes. + ### Instrument A Handler +Finally, surface the custom instrumentation inside your Lambda handler. `AwsLambda.Host` injects any registered services, so you can receive both `IService` and `Instrumentation` directly in the handler signature. + ```csharp title="Function.cs" linenums="1" using AwsLambda.Host.Builder; @@ -272,3 +285,5 @@ internal static class Function } } ``` + +The handler starts a root `Activity` before invoking downstream services, ensuring any spans produced inside `Service.GetMessage` automatically nest beneath it. Because `UseOpenTelemetryTracing()` already wires up the Lambda envelope instrumentation, your manual spans flow into the same trace, giving full visibility from the trigger event through your business logic and custom metrics. From fdc13384b9e754c371322a4a7892e03f8921f1fd Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 09:55:36 -0500 Subject: [PATCH 32/65] feat(docs): expand OpenTelemetry guide with metrics instrumentation and shutdown improvements - Added guidance for registering meters and exporting metrics alongside traces. - Updated shutdown helper documentation with details on `OnShutdownFlushTracer` and `OnShutdownFlushMeter`. - Included notes on required DI service registrations for proper provider functionality. - Enhanced examples with metrics instrumentation in services for better correlation with tracing. --- docs/features/open_telemetry.md | 40 ++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 2a11fa91..f0f5b713 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -62,15 +62,20 @@ builder ResourceBuilder.CreateDefault().AddService("MyService", serviceVersion: "1.0.0") ); tracing.AddConsoleExporter();// (4)! + }) + .WithMetrics(metrics => + { + metrics.AddMeter("MyService");// (5)! + metrics.AddConsoleExporter();// (6)! }); await using var lambda = builder.Build(); -lambda.UseOpenTelemetryTracing();// (5)! +lambda.UseOpenTelemetryTracing();// (7)! -lambda.OnShutdownFlushOpenTelemetry();// (6)! +lambda.OnShutdownFlushOpenTelemetry();// (8)! -lambda.MapHandler(// (7)! +lambda.MapHandler(// (9)! async ([Event] Request request, ILogger logger, CancellationToken cancellationToken) => { logger.LogInformation("Responding to {Name}", request.Name); @@ -92,9 +97,11 @@ internal record Response(string Message); 2. Enable AWS Lambda configurations 3. Add a custom source for tracing 4. Export traces to a collector, in this case the console -5. Enable OpenTelemetry tracing in the Lambda host through middleware. -6. (Optional) Flush the OpenTelemetry traces at the end of the Lambda execution. -7. Write your Lambda handler like normal. +5. Register a named meter so custom metrics (like `NameMetrics`) can emit data +6. Export metrics alongside traces +7. Enable OpenTelemetry tracing in the Lambda host through middleware. +8. (Optional) Flush the OpenTelemetry traces at the end of the Lambda execution. +9. Write your Lambda handler like normal. !!! tip OpenTelemetry tracing can be configured in multiple ways, including manually creating a trace provider using the [OpenTelemetry](https://www.nuget.org/packages/OpenTelemetry), or through registering OpenTelemetry services in your DI container using [OpenTelemetry.Extensions.Hosting](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting). @@ -116,7 +123,7 @@ Here's the step-by-step process: This entire process happens during compilation, resulting in highly optimized code that instruments your handler without any reflection overhead at runtime. -Similarly, `OnShutdownFlushOpenTelemetry()` is an interceptor that registers a shutdown hook. This hook flushes the OpenTelemetry providers, ensuring buffered telemetry is sent before the Lambda execution environment terminates. +In contrast, the shutdown helpers (`OnShutdownFlushOpenTelemetry`, `OnShutdownFlushTracer`, and `OnShutdownFlushMeter`) are regular extension methods. They execute as-is at runtime and use the registered `TracerProvider`/`MeterProvider` instances to force-flush telemetry before Lambda freezes the environment. --- @@ -162,6 +169,9 @@ The following methods are available to be called on the `LambdaApplication` inst For most applications, calling `lambda.OnShutdownFlushOpenTelemetry()` is sufficient to ensure all telemetry is flushed. If your application only uses tracing or metrics, but not both, you can use the more specific methods for clarity. +!!! note + These methods call `GetRequiredService()` and `GetRequiredService()`. Make sure those providers are registered (via `.AddOpenTelemetry().WithTracing(...)` / `.WithMetrics(...)`) before invoking the shutdown helpers, otherwise the application will throw during startup. + All three methods also accept an optional `timeoutMilliseconds` parameter. This allows you to specify a maximum duration for the flush operation. Importantly, these flush operations are non-blocking and respect the provided `CancellationToken`, ensuring they can gracefully exit if the Lambda execution environment signals a shutdown before the timeout elapses. This combined approach offers robust control over the flush duration within the limited time available during a Lambda shutdown. !!! Warning @@ -238,26 +248,30 @@ Once the reusable helpers exist, wrap service logic in spans to capture timing, ```csharp title="NameService.cs" linenums="1" using System.Diagnostics; -public class NameService +namespace AwsLambda.Host.Example.OpenTelemetry; + +internal class NameService(Instrumentation instrumentation, NameMetrics nameMetrics) { - private static readonly ActivitySource Source = new("MyApplication.NameService"); + private readonly ActivitySource _activitySource = instrumentation.ActivitySource; - public string GetFullName(string name) + public async Task GetFullName(string name, CancellationToken cancellationToken = default) { - using var activity = Source.StartActivity("GetFullName"); + using var activity = _activitySource.StartActivity("GetFullName"); activity?.SetTag("input.name", name); - // ... some work ... + await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken); var fullName = $"{name} Smith"; activity?.SetTag("output.fullname", fullName); + nameMetrics.ProcessName(name); + return fullName; } } ``` -The `using var activity = ...` pattern mirrors the BCL samples and guarantees spans finish even when exceptions are thrown. You can call into `NameMetrics.ProcessName` inside the same scope so traces and metrics share correlated attributes. +The `using var activity = ...` pattern mirrors the BCL samples and guarantees spans finish even when exceptions are thrown. Because the service receives both `Instrumentation` and `NameMetrics` through DI, every span and counter entry shares the same source name and tags, keeping traces and metrics correlated. ### Instrument A Handler From 452c2fdd80d41cac22c1da22f99e47a56dcbcdd5 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 09:57:35 -0500 Subject: [PATCH 33/65] chore(docs): fix typo in OpenTelemetry guide section title - Corrected "Gracefully Shutting & Cleaning Up" to "Gracefully Shutdown & Cleaning Up". - Improved title consistency and readability throughout the section. --- docs/features/open_telemetry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index f0f5b713..7bde444e 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -153,7 +153,7 @@ This method call acts as a compile-time trigger for a source generator. The gene Under the covers, the source generator performs a critical task. It inspects the delegate you provided to `MapHandler` to determine the specific input and output types of your function (e.g., `APIGatewayProxyRequest`, `SQSEvent`). It then uses these types to generate a call to a generic helper method. This ensures that the underlying `OpenTelemetry.Instrumentation.AWSLambda` package receives a strongly-typed request object. By preserving the specific event type, the OpenTelemetry instrumentation can correctly extract context and attributes, such as trace parent headers from an API Gateway request, ensuring proper distributed trace propagation. -### Gracefully Shutting & Cleaning Up +### Gracefully Shutdown & Cleaning Up The OpenTelemetry `TracerProvider` and `MeterProvider` services both implement `IDisposable`. When the dependency injection container is disposed of during a normal application shutdown, it should trigger these providers to automatically flush any buffered telemetry. However, in a serverless environment where the lifecycle can be abrupt, this disposal is not always guaranteed to complete before the execution environment is frozen. From 97bdd994172fcac4ca2e3ffcf2a9465de8861767 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 10:23:52 -0500 Subject: [PATCH 34/65] feat(docs): enhance envelopes guide with installation steps and async usage examples - Added `Installation` section detailing package-specific commands for envelope usage. - Updated example to demonstrate async request handling with `CancellationToken` support. - Replaced `AwsLambda.Host.Abstractions.Options` references with `AwsLambda.Host.Options`. - Improved clarity of type-safe deserialized properties in guided explanations. --- docs/features/envelopes.md | 68 ++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/features/envelopes.md b/docs/features/envelopes.md index 4fee0a5b..0f258bab 100644 --- a/docs/features/envelopes.md +++ b/docs/features/envelopes.md @@ -2,7 +2,7 @@ **What are Envelopes?** -Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) and add a `BodyContent` property that provides type-safe access to deserialized message payloads. Instead of manually parsing JSON strings from event bodies, you get strongly-typed objects with full IDE support and compile-time type checking. +Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) and add typed content properties (`BodyContent`, `MessageContent`, etc.) that provide type-safe access to deserialized message payloads. Instead of manually parsing JSON strings from event bodies, you get strongly-typed objects with full IDE support and compile-time type checking. **Key Benefits**: @@ -30,6 +30,20 @@ Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIG ## Quick Start +### Installation + +Install only the envelope packages that match the AWS event sources you plan to handle. For example: + +```bash +# Core SQS envelope +dotnet add package AwsLambda.Host.Envelopes.Sqs + +# API Gateway (REST/WebSocket) envelope +dotnet add package AwsLambda.Host.Envelopes.ApiGateway +``` + +Repeat for SNS, Kinesis, Kafka, ALB, etc., as needed. + ### Using Envelopes This example demonstrates the envelope pattern using SQS, but the same pattern applies to all envelope types (SNS, API Gateway, Kinesis, etc.) - simply swap `SqsEnvelope` for the appropriate envelope type. @@ -299,7 +313,7 @@ A custom event with metadata and a nested JSON payload: ```csharp title="CustomRequestEvent.cs" linenums="1" using System.Text.Json; using System.Text.Json.Serialization; -using AwsLambda.Host.Abstractions.Options; +using AwsLambda.Host.Options; using AwsLambda.Host.Envelopes; public class CustomRequestEvent : IRequestEnvelope @@ -359,7 +373,7 @@ Custom envelopes for both incoming requests and outgoing responses: ```csharp title="ApiEnvelopes.cs" linenums="1" using System.Text.Json; using System.Text.Json.Serialization; -using AwsLambda.Host.Abstractions.Options; +using AwsLambda.Host.Options; using AwsLambda.Host.Envelopes; // Request envelope @@ -402,31 +416,37 @@ public record ResponsePayload(bool Success, string Message, object? Data = null) **Usage:** ```csharp title="Program.cs" linenums="1" -lambda.MapHandler(request => -{ - var response = new ApiResponse { StatusCode = 200 }; - - if (request.BodyContent is null) +lambda.MapHandler( + async ([Event] ApiRequest request, CancellationToken cancellationToken) => { - response.StatusCode = 400; + var response = new ApiResponse { StatusCode = 200 }; + + if (request.BodyContent is null) + { + response.StatusCode = 400; + response.BodyContent = new ResponsePayload( + Success: false, + Message: "Invalid request body" + ); + return response; + } + + // Process the request + var result = await ProcessActionAsync( + request.BodyContent.Action, + request.BodyContent.Parameters, + cancellationToken + ); + response.BodyContent = new ResponsePayload( - Success: false, - Message: "Invalid request body" + Success: true, + Message: "Action completed", + Data: result ); + return response; } - - // Process the request - var result = ProcessAction(request.BodyContent.Action, request.BodyContent.Parameters); - - response.BodyContent = new ResponsePayload( - Success: true, - Message: "Action completed", - Data: result - ); - - return response; -}); +); ``` --- @@ -438,7 +458,7 @@ A custom event with multiple records, similar to SQS batch processing: ```csharp title="BatchEvent.cs" linenums="1" using System.Text.Json; using System.Text.Json.Serialization; -using AwsLambda.Host.Abstractions.Options; +using AwsLambda.Host.Options; using AwsLambda.Host.Envelopes; public class BatchEvent : IRequestEnvelope From 00d316ab380a6e8f04974b60b3a1bd4278d59c64 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 10:24:54 -0500 Subject: [PATCH 35/65] feat(docs): remove outdated deployment and project structure guides - Deleted `deployment.md` and `project-structure.md` under `docs/guides`. - Simplified documentation by removing redundant and legacy content. - Prepared for upcoming additions aligned with modern cloud deployment strategies. --- docs/getting-started/project-structure.md | 677 ------------------ docs/guides/deployment.md | 814 ---------------------- mkdocs.yml | 2 - 3 files changed, 1493 deletions(-) delete mode 100644 docs/getting-started/project-structure.md delete mode 100644 docs/guides/deployment.md diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md deleted file mode 100644 index 826711b1..00000000 --- a/docs/getting-started/project-structure.md +++ /dev/null @@ -1,677 +0,0 @@ -# Project Structure - -As your Lambda function grows from a simple handler to a production service, proper organization becomes essential. This guide shows you how to structure Lambda projects for maintainability, testability, and scalability. - -## Introduction - -Good project structure helps you: - -- **Find code quickly** – Organized by responsibility -- **Test effectively** – Clear separation of concerns -- **Scale smoothly** – Easy to add new features -- **Maintain easily** – Consistent patterns - -We'll progress from simple to complex structures, so you can choose what fits your needs. - -## Simple Lambda Structure - -For straightforward Lambdas with a single responsibility: - -``` -MyLambda/ -├── MyLambda.csproj # Project file -├── Program.cs # Entry point + handler -├── Models/ -│ ├── Request.cs # Input model -│ └── Response.cs # Output model -├── Services/ -│ ├── IMyService.cs # Service interface -│ └── MyService.cs # Service implementation -└── appsettings.json # Configuration (optional) -``` - -**When to use:** -- Single event source -- Simple business logic -- 1-2 services -- Under 500 lines of code - -## Modular Service Structure - -For complex Lambdas with multiple services and responsibilities: - -``` -OrderProcessingLambda/ -├── OrderProcessingLambda.csproj -├── Program.cs # Entry point + DI registration -├── Models/ -│ ├── Order.cs -│ ├── OrderItem.cs -│ ├── OrderRequest.cs -│ └── OrderResponse.cs -├── Services/ -│ ├── IOrderService.cs -│ ├── OrderService.cs -│ ├── IInventoryService.cs -│ ├── InventoryService.cs -│ ├── IPaymentService.cs -│ └── PaymentService.cs -├── Repositories/ -│ ├── IOrderRepository.cs -│ └── OrderRepository.cs -├── Middleware/ -│ └── ValidationMiddleware.cs -├── Configuration/ -│ └── OrderProcessingOptions.cs -└── appsettings.json -``` - -**When to use:** -- Multiple services -- Complex business logic -- Data access layers -- Reusable middleware -- Configuration management - -## Program.cs Organization - -Your `Program.cs` file is the entry point for your Lambda. Following a consistent organization pattern makes it easier to understand and maintain. - -### Recommended Order - -```csharp title="Program.cs" linenums="1" -// 1. Using statements (explicit, grouped logically) -using System; -using System.Threading; -using System.Threading.Tasks; -using AwsLambda.Host; -using Microsoft.Extensions.DependencyInjection; -using MyLambda.Services; -using MyLambda.Models; - -// 2. Create builder -var builder = LambdaApplication.CreateBuilder(); - -// 3. Configure options (if needed) -builder.Services.Configure( - builder.Configuration.GetSection("OrderProcessing") -); - -// 4. Register services (grouped by lifetime) -// Singletons first (shared across invocations) -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -// Scoped services (per invocation) -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// 5. Lifecycle hooks -builder.Services.AddOnInit(async (services, ct) => -{ - // Initialization logic -}); - -builder.Services.AddOnShutdown(async (services, ct) => -{ - // Cleanup logic -}); - -// 6. Build application -var lambda = builder.Build(); - -// 7. Register middleware (in order of execution) -lambda.UseMiddleware(); -lambda.UseMiddleware(); - -// 8. Register handler -lambda.MapHandler(([Event] OrderRequest request, IOrderService service) => - service.ProcessAsync(request) -); - -// 9. Run -await lambda.RunAsync(); -``` - -!!! tip "Consistent Organization" - Following this order consistently across your Lambda functions makes them easier to understand and maintain. - -## Extracting Services into Files - -As your `Program.cs` grows, extract services into separate files. - -### Before (Inline) - -```csharp title="Program.cs" -// Everything in one file -public interface IGreetingService -{ - string GetGreeting(string name); -} - -public class GreetingService : IGreetingService -{ - public string GetGreeting(string name) => $"Hello, {name}!"; -} - -var builder = LambdaApplication.CreateBuilder(); -builder.Services.AddSingleton(); -// ... -``` - -**Problems:** -- Hard to test in isolation -- File becomes very long -- Difficult to reuse -- Poor separation of concerns - -### After (Extracted) - -```csharp title="Services/IGreetingService.cs" -namespace MyLambda.Services; - -public interface IGreetingService -{ - string GetGreeting(string name); -} -``` - -```csharp title="Services/GreetingService.cs" -namespace MyLambda.Services; - -public class GreetingService : IGreetingService -{ - public string GetGreeting(string name) => $"Hello, {name}!"; -} -``` - -```csharp title="Program.cs" -using MyLambda.Services; - -var builder = LambdaApplication.CreateBuilder(); -builder.Services.AddSingleton(); -// ... -``` - -**Benefits:** -- Each file has one responsibility -- Easy to find and modify -- Testable in isolation -- Reusable across projects - -!!! tip "When to Extract" - Extract to separate files when: - - Service has more than 20 lines - - You need to test it in isolation - - It could be reused elsewhere - - Program.cs exceeds 100 lines - -## Test Project Structure - -Organize tests to mirror your source code structure: - -``` -MyLambda.Tests/ -├── MyLambda.Tests.csproj -├── Services/ -│ ├── OrderServiceTests.cs -│ └── InventoryServiceTests.cs -├── Handlers/ -│ └── OrderHandlerTests.cs -├── Middleware/ -│ └── ValidationMiddlewareTests.cs -├── Fixtures/ -│ ├── TestFixture.cs # Shared test setup -│ └── TestData.cs # Test data builders -└── Integration/ - └── LambdaIntegrationTests.cs -``` - -### Unit Test Example - -```csharp title="Services/OrderServiceTests.cs" -using Xunit; -using NSubstitute; -using MyLambda.Services; -using MyLambda.Models; - -public class OrderServiceTests -{ - [Fact] - public async Task ProcessAsync_ValidOrder_ReturnsSuccess() - { - // Arrange - var repository = Substitute.For(); - repository.SaveAsync(Arg.Any()) - .Returns(new SaveResult { Success = true, Id = "123" }); - - var service = new OrderService(repository); - var order = new Order("123", 99.99m); - - // Act - var result = await service.ProcessAsync(order); - - // Assert - Assert.True(result.Success); - Assert.Equal("123", result.OrderId); - await repository.Received(1).SaveAsync(order); - } - - [Fact] - public async Task ProcessAsync_InvalidOrder_ThrowsValidationException() - { - // Arrange - var repository = Substitute.For(); - var service = new OrderService(repository); - var order = new Order("", -1m); // Invalid order - - // Act & Assert - await Assert.ThrowsAsync(() => - service.ProcessAsync(order) - ); - } -} -``` - -!!! info "Testing Framework" - This project uses xUnit with NSubstitute for mocking. Adjust patterns to match your testing framework of choice. - -## Configuration Management - -Manage configuration externally rather than hardcoding values. - -### appsettings.json - -```json title="appsettings.json" -{ - "OrderProcessing": { - "MaxRetries": 3, - "TimeoutSeconds": 30, - "EnableCaching": true - }, - "Database": { - "ConnectionString": "Server=localhost;Database=orders", - "CommandTimeout": 30 - }, - "ExternalApi": { - "BaseUrl": "https://api.example.com", - "ApiKey": "" // Set via environment variable - } -} -``` - -### Options Class - -Create strongly-typed configuration classes: - -```csharp title="Configuration/OrderProcessingOptions.cs" -namespace MyLambda.Configuration; - -public class OrderProcessingOptions -{ - public int MaxRetries { get; init; } - public int TimeoutSeconds { get; init; } - public bool EnableCaching { get; init; } -} -``` - -### Binding Configuration - -Bind configuration sections to options classes: - -```csharp title="Program.cs" -builder.Services.Configure( - builder.Configuration.GetSection("OrderProcessing") -); -``` - -### Using Options in Services - -Inject `IOptions` into your services: - -```csharp title="Services/OrderService.cs" -using Microsoft.Extensions.Options; - -public class OrderService : IOrderService -{ - private readonly OrderProcessingOptions _options; - private readonly IOrderRepository _repository; - - public OrderService( - IOptions options, - IOrderRepository repository) - { - _options = options.Value; - _repository = repository; - } - - public async Task ProcessAsync(Order order) - { - // Use configuration - if (_options.EnableCaching) - { - // Check cache... - } - - for (int retry = 0; retry < _options.MaxRetries; retry++) - { - try - { - return await _repository.SaveAsync(order); - } - catch (Exception ex) when (retry < _options.MaxRetries - 1) - { - // Retry logic - await Task.Delay(TimeSpan.FromSeconds(1)); - } - } - - throw new Exception("Max retries exceeded"); - } -} -``` - -## Environment-Specific Configuration - -Use multiple configuration files for different environments: - -### File Structure - -``` -MyLambda/ -├── appsettings.json # Base configuration -├── appsettings.Development.json # Development overrides -└── appsettings.Production.json # Production overrides -``` - -### Configuration Loading - -```csharp title="Program.cs" -var builder = LambdaApplication.CreateBuilder(); - -// Configuration is automatically loaded in this order: -// 1. appsettings.json (base) -// 2. appsettings.{Environment}.json (environment-specific) -// 3. Environment variables (highest priority) - -// Add additional configuration sources -builder.Configuration.AddEnvironmentVariables(); -``` - -### Environment Variables - -Access environment variables directly or through configuration: - -```csharp -// Direct access -var apiKey = Environment.GetEnvironmentVariable("API_KEY"); - -// Through configuration (recommended) -var apiKey = builder.Configuration["ExternalApi:ApiKey"]; -``` - -!!! warning "Never Commit Secrets" - Never commit API keys, connection strings, or other secrets to source control. Use environment variables or AWS Secrets Manager. - -## Secrets Management - -### AWS Secrets Manager Integration - -```csharp title="Program.cs" -using Amazon; -using Amazon.Extensions.Configuration.SystemsManager; - -var builder = LambdaApplication.CreateBuilder(); - -// Add AWS Secrets Manager as configuration source -builder.Configuration.AddSecretsManager( - region: RegionEndpoint.USEast1, - configurator: options => - { - options.SecretFilter = entry => entry.Name.StartsWith("MyLambda/"); - options.PollingInterval = TimeSpan.FromMinutes(5); - } -); -``` - -### Environment Variables Pattern - -Set sensitive values via Lambda environment variables: - -```csharp -// Lambda environment variable: DATABASE_CONNECTION_STRING -var connectionString = builder.Configuration["DATABASE_CONNECTION_STRING"]; - -// Or through options binding -public class DatabaseOptions -{ - public string ConnectionString { get; init; } = ""; -} - -builder.Services.Configure( - builder.Configuration.GetSection("Database") -); -``` - -## Deployment Structure - -### SAM Template Organization - -``` -MyLambda/ -├── src/ -│ └── MyLambda/ # Lambda source code -│ ├── Program.cs -│ └── MyLambda.csproj -├── test/ -│ └── MyLambda.Tests/ # Unit tests -├── template.yaml # SAM template -├── samconfig.toml # SAM configuration -└── events/ - ├── order-event.json # Test event 1 - └── error-event.json # Test event 2 -``` - -### CDK Project Structure - -``` -MyLambdaStack/ -├── src/ -│ ├── MyLambdaStack/ # CDK infrastructure code -│ │ ├── MyLambdaStack.cs -│ │ └── MyLambdaStack.csproj -│ └── MyLambda/ # Lambda function code -│ ├── Program.cs -│ └── MyLambda.csproj -├── test/ -│ └── MyLambdaStack.Tests/ -├── cdk.json -└── README.md -``` - -## Anti-Patterns to Avoid - -### ❌ Don't: Mix Concerns in Program.cs - -```csharp -// BAD: Business logic directly in Program.cs -lambda.MapHandler(([Event] Order order) => -{ - // 50+ lines of business logic - // Database queries - // External API calls - // Complex calculations - // Validation logic - return new OrderResponse(/* ... */); -}); -``` - -### ✅ Do: Extract to Services - -```csharp -// GOOD: Delegate to service -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.ProcessAsync(order) -); -``` - ---- - -### ❌ Don't: Register Everything as Singleton - -```csharp -// BAD: Wrong lifetime -builder.Services.AddSingleton(); -// Repository with per-request state should be Scoped! -``` - -### ✅ Do: Use Appropriate Lifetimes - -```csharp -// GOOD: Correct lifetime -builder.Services.AddSingleton(); // Stateless, shared -builder.Services.AddScoped(); // Per-request state -``` - ---- - -### ❌ Don't: Hardcode Configuration - -```csharp -// BAD: Hardcoded values -public class OrderService -{ - private const int MaxRetries = 3; - private const string ApiUrl = "https://api.example.com"; - private const int Timeout = 30; -} -``` - -### ✅ Do: Use Configuration Options - -```csharp -// GOOD: Configuration-driven -public class OrderService -{ - private readonly OrderProcessingOptions _options; - - public OrderService(IOptions options) - { - _options = options.Value; - } -} -``` - ---- - -### ❌ Don't: Put Models Everywhere - -```csharp -// BAD: Models mixed with logic -public class OrderService -{ - public record OrderRequest(string Id); // Don't define here - public record OrderResponse(bool Success); // Don't define here - - public OrderResponse Process(OrderRequest request) { ... } -} -``` - -### ✅ Do: Organize Models in Dedicated Folder - -```csharp -// GOOD: Models in Models/ folder -// Models/OrderRequest.cs -namespace MyLambda.Models; -public record OrderRequest(string Id, decimal Amount); - -// Models/OrderResponse.cs -namespace MyLambda.Models; -public record OrderResponse(string OrderId, bool Success); -``` - ---- - -### ❌ Don't: Use Magic Strings - -```csharp -// BAD: Magic strings -var connectionString = builder.Configuration["ConnectionStrings:Default"]; -var timeout = int.Parse(builder.Configuration["Timeout"]); -``` - -### ✅ Do: Use Strongly-Typed Options - -```csharp -// GOOD: Strongly-typed configuration -public class DatabaseOptions -{ - public string ConnectionString { get; init; } = ""; - public int CommandTimeout { get; init; } -} - -builder.Services.Configure( - builder.Configuration.GetSection("Database") -); -``` - ---- - -### ❌ Don't: Ignore Async/Await - -```csharp -// BAD: Blocking async code -lambda.MapHandler(([Event] Order order, IOrderService service) => -{ - var result = service.ProcessAsync(order).Result; // DON'T! - return result; -}); -``` - -### ✅ Do: Use Async/Await Properly - -```csharp -// GOOD: Proper async/await -lambda.MapHandler(async ([Event] Order order, IOrderService service) => -{ - var result = await service.ProcessAsync(order); - return result; -}); -``` - -## Key Takeaways - -1. **Start Simple**: Single file for simple Lambdas, expand as complexity grows -2. **Extract Early**: Move services to separate files before they become unwieldy -3. **Organize by Lifetime**: Group service registrations by Singleton vs Scoped -4. **Test Structure Mirrors Source**: Keep test organization consistent with source code -5. **Configuration Over Code**: Use `appsettings.json` and options pattern -6. **Secrets External**: Never commit secrets; use environment variables or AWS Secrets Manager -7. **Consistent Ordering**: Follow Program.cs organization pattern across all Lambdas -8. **Avoid Anti-Patterns**: Don't mix concerns, hardcode values, or use wrong lifetimes - -## Next Steps - -You now understand how to structure Lambda projects for maintainability and scalability. - -### Continue Learning - -- **[Middleware Patterns](/guides/middleware.md)** – Build reusable middleware components -- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns -- **[Testing Strategies](/guides/testing.md)** – Comprehensive testing approaches -- **[Deployment Best Practices](/guides/deployment.md)** – CI/CD and production deployments - -### Explore Features - -- **[Envelopes](/features/envelopes/)** – Type-safe event source integration -- **[OpenTelemetry](/features/opentelemetry.md)** – Add distributed tracing -- **[AOT Compilation](/advanced/aot-compilation.md)** – Optimize for fastest cold starts - -### Browse Examples - -- **[Examples](/examples/)** – Complete working examples -- **[API Reference](/api-reference/)** – Detailed API documentation - ---- - -Congratulations! You've completed the Getting Started guide. You now have the knowledge to build, organize, and deploy production-ready Lambda functions with aws-lambda-host. diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md deleted file mode 100644 index d2da865e..00000000 --- a/docs/guides/deployment.md +++ /dev/null @@ -1,814 +0,0 @@ -# Deployment - -This guide covers deploying AWS Lambda functions built with aws-lambda-host using various Infrastructure as Code (IaC) tools and CI/CD pipelines. - ---- - -## Deployment Options - -You can deploy Lambda functions using: - -- **AWS SAM (Serverless Application Model)** - AWS-native IaC framework optimized for serverless -- **AWS CDK (Cloud Development Kit)** - Type-safe infrastructure using C# -- **Terraform** - Cloud-agnostic IaC tool -- **Manual Deployment** - AWS CLI or Console -- **CI/CD Pipelines** - Automated deployment with GitHub Actions, GitLab CI, or Azure DevOps - ---- - -## Project Configuration - -### Standard Lambda (.csproj) - -For traditional Lambda functions with JIT compilation: - -```xml title="MyLambda.csproj" linenums="1" - - - Exe - net8.0 - enable - enable - - - Lambda - true - true - - - $(InterceptorsNamespaces);AwsLambda.Host - - - - - - - -``` - -**Build and publish**: - -```bash -dotnet publish -c Release -o ./publish -``` - -### Native AOT Lambda (.csproj) - -For optimized Lambda functions with Native AOT (faster cold starts, smaller package size): - -```xml title="MyLambdaAot.csproj" linenums="1" - - - Exe - net8.0 - enable - enable - - - true - true - false - full - true - - - bootstrap - - - Lambda - true - - - $(InterceptorsNamespaces);AwsLambda.Host - - - - - - - -``` - -**Build and publish for AOT**: - -```bash -# Publish with Native AOT (requires Docker for cross-compilation on non-Linux) -dotnet publish -c Release -o ./publish /p:PublishAot=true - -# Or use the AWS Lambda build container -docker run --rm -v $(pwd):/var/task public.ecr.aws/sam/build-dotnet8:latest \ - dotnet publish -c Release -o /var/task/publish /p:PublishAot=true -``` - -!!! tip "Native AOT Benefits" - - **Faster cold starts**: 50-80% reduction in cold start times - - **Smaller package size**: Trimming removes unused code - - **Lower memory usage**: More efficient resource utilization - - **Predictable performance**: No JIT compilation overhead - ---- - -## AWS SAM Deployment - -AWS SAM provides the simplest deployment experience for Lambda functions. - -### SAM Template - -Create a `template.yaml` file in your project root: - -```yaml title="template.yaml" linenums="1" -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: My Lambda Function - -Globals: - Function: - Timeout: 30 - MemorySize: 512 - Runtime: provided.al2023 - Architectures: - - x86_64 - -Resources: - MyFunction: - Type: AWS::Serverless::Function - Properties: - Handler: bootstrap - CodeUri: ./publish - Environment: - Variables: - ENVIRONMENT: production - LOG_LEVEL: info - Events: - ApiEvent: - Type: Api - Properties: - Path: /orders - Method: post - -Outputs: - MyFunctionArn: - Description: Lambda Function ARN - Value: !GetAtt MyFunction.Arn - ApiUrl: - Description: API Gateway endpoint - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/orders" -``` - -### SAM Commands - -```bash title="Deployment commands" -# Build the Lambda function -dotnet publish -c Release -o ./publish - -# Package and deploy (first time - guided) -sam deploy --guided - -# Subsequent deployments -sam deploy - -# Delete the stack -sam delete -``` - -**sam deploy --guided** prompts: - -``` -Stack Name: my-lambda-stack -AWS Region: us-east-1 -Confirm changes before deploy: Y -Allow SAM CLI IAM role creation: Y -Disable rollback: N -Save arguments to configuration file: Y -SAM configuration file: samconfig.toml -``` - -### SAM with Native AOT - -```yaml title="template.yaml (AOT)" linenums="1" -Resources: - MyFunctionAot: - Type: AWS::Serverless::Function - Metadata: - BuildMethod: dotnet8 - BuildArchitecture: x86_64 - Properties: - Handler: bootstrap - Runtime: provided.al2023 - CodeUri: ./ - Architectures: - - x86_64 - MemorySize: 512 - Timeout: 30 -``` - -```bash -# SAM builds with Docker automatically -sam build - -# Deploy -sam deploy --guided -``` - ---- - -## AWS CDK Deployment - -AWS CDK allows you to define infrastructure using C#. - -### CDK Stack - -```csharp title="MyLambdaStack.cs" linenums="1" -using Amazon.CDK; -using Amazon.CDK.AWS.Lambda; -using Amazon.CDK.AWS.APIGateway; -using Constructs; - -namespace MyCdkApp -{ - public class MyLambdaStack : Stack - { - public MyLambdaStack(Construct scope, string id, IStackProps props = null) - : base(scope, id, props) - { - // Lambda function with Native AOT - var myFunction = new Function(this, "MyFunction", new FunctionProps - { - Runtime = Runtime.PROVIDED_AL2023, - Handler = "bootstrap", - Code = Code.FromAsset("../MyLambda/publish"), - MemorySize = 512, - Timeout = Duration.Seconds(30), - Architecture = Architecture.X86_64, - Environment = new Dictionary - { - ["ENVIRONMENT"] = "production", - ["LOG_LEVEL"] = "info" - } - }); - - // API Gateway - var api = new RestApi(this, "MyApi", new RestApiProps - { - RestApiName = "My API", - Description = "API for My Lambda Function" - }); - - var integration = new LambdaIntegration(myFunction); - var orders = api.Root.AddResource("orders"); - orders.AddMethod("POST", integration); - - // Outputs - new CfnOutput(this, "FunctionArn", new CfnOutputProps - { - Value = myFunction.FunctionArn, - Description = "Lambda Function ARN" - }); - - new CfnOutput(this, "ApiUrl", new CfnOutputProps - { - Value = api.Url, - Description = "API Gateway URL" - }); - } - } -} -``` - -### CDK Program - -```csharp title="Program.cs" linenums="1" -using Amazon.CDK; - -namespace MyCdkApp -{ - class Program - { - static void Main(string[] args) - { - var app = new App(); - - new MyLambdaStack(app, "MyLambdaStack", new StackProps - { - Env = new Amazon.CDK.Environment - { - Account = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_ACCOUNT"), - Region = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_REGION") - } - }); - - app.Synth(); - } - } -} -``` - -### CDK Commands - -```bash title="CDK deployment" -# Install CDK CLI -npm install -g aws-cdk - -# Initialize CDK project (one time) -mkdir cdk && cd cdk -cdk init app --language csharp - -# Build Lambda function -cd ../MyLambda -dotnet publish -c Release -o ./publish - -# Deploy -cd ../cdk -cdk deploy - -# Destroy -cdk destroy -``` - ---- - -## Terraform Deployment - -Terraform provides cloud-agnostic infrastructure management. - -### Terraform Configuration - -```hcl title="main.tf" linenums="1" -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - } -} - -provider "aws" { - region = var.aws_region -} - -# Lambda IAM Role -resource "aws_iam_role" "lambda_role" { - name = "my-lambda-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "lambda.amazonaws.com" - } - } - ] - }) -} - -resource "aws_iam_role_policy_attachment" "lambda_basic" { - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.lambda_role.name -} - -# Lambda Function -resource "aws_lambda_function" "my_function" { - filename = "../MyLambda/publish.zip" - function_name = "my-lambda-function" - role = aws_iam_role.lambda_role.arn - handler = "bootstrap" - runtime = "provided.al2023" - architectures = ["x86_64"] - memory_size = 512 - timeout = 30 - - source_code_hash = filebase64sha256("../MyLambda/publish.zip") - - environment { - variables = { - ENVIRONMENT = "production" - LOG_LEVEL = "info" - } - } -} - -# API Gateway -resource "aws_apigatewayv2_api" "api" { - name = "my-api" - protocol_type = "HTTP" -} - -resource "aws_apigatewayv2_integration" "lambda_integration" { - api_id = aws_apigatewayv2_api.api.id - integration_type = "AWS_PROXY" - integration_uri = aws_lambda_function.my_function.invoke_arn -} - -resource "aws_apigatewayv2_route" "post_orders" { - api_id = aws_apigatewayv2_api.api.id - route_key = "POST /orders" - target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" -} - -resource "aws_apigatewayv2_stage" "default" { - api_id = aws_apigatewayv2_api.api.id - name = "$default" - auto_deploy = true -} - -resource "aws_lambda_permission" "api_gateway" { - statement_id = "AllowAPIGatewayInvoke" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.my_function.function_name - principal = "apigateway.amazonaws.com" - source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*" -} - -# Outputs -output "function_arn" { - value = aws_lambda_function.my_function.arn -} - -output "api_url" { - value = aws_apigatewayv2_stage.default.invoke_url -} -``` - -```hcl title="variables.tf" linenums="1" -variable "aws_region" { - description = "AWS region" - type = string - default = "us-east-1" -} -``` - -### Terraform Commands - -```bash title="Terraform deployment" -# Build and package Lambda -cd MyLambda -dotnet publish -c Release -o ./publish -cd publish && zip -r ../publish.zip . && cd .. - -# Initialize Terraform -cd ../terraform -terraform init - -# Plan deployment -terraform plan - -# Apply deployment -terraform apply - -# Destroy -terraform destroy -``` - ---- - -## CI/CD with GitHub Actions - -Automate builds and deployments using GitHub Actions. - -### GitHub Actions Workflow (SAM) - -```yaml title=".github/workflows/deploy.yml" linenums="1" -name: Deploy Lambda - -on: - push: - branches: - - main - -permissions: - id-token: write - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Build Lambda - run: | - cd src/MyLambda - dotnet publish -c Release -o ./publish - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsRole - aws-region: us-east-1 - - - name: Setup SAM - uses: aws-actions/setup-sam@v2 - - - name: Deploy with SAM - run: | - sam deploy --no-confirm-changeset --no-fail-on-empty-changeset -``` - -### GitHub Actions Workflow (Native AOT with Docker) - -```yaml title=".github/workflows/deploy-aot.yml" linenums="1" -name: Deploy Lambda (AOT) - -on: - push: - branches: - - main - -permissions: - id-token: write - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build Lambda with Native AOT - run: | - docker run --rm \ - -v $(pwd)/src/MyLambda:/var/task \ - -v $(pwd)/publish:/var/task/publish \ - public.ecr.aws/sam/build-dotnet8:latest \ - dotnet publish -c Release -o /var/task/publish /p:PublishAot=true - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsRole - aws-region: us-east-1 - - - name: Setup SAM - uses: aws-actions/setup-sam@v2 - - - name: Deploy with SAM - run: | - sam deploy --no-confirm-changeset --no-fail-on-empty-changeset -``` - -### CDK Deployment with GitHub Actions - -```yaml title=".github/workflows/cdk-deploy.yml" linenums="1" -name: CDK Deploy - -on: - push: - branches: - - main - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Build Lambda - run: | - cd src/MyLambda - dotnet publish -c Release -o ./publish - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} - aws-region: us-east-1 - - - name: Install CDK - run: npm install -g aws-cdk - - - name: Deploy CDK Stack - run: | - cd cdk - dotnet build - cdk deploy --require-approval never -``` - ---- - -## Manual Deployment - -### Using AWS CLI - -```bash title="Manual deployment with AWS CLI" -# Build Lambda -dotnet publish -c Release -o ./publish - -# Package -cd publish && zip -r ../function.zip . && cd .. - -# Create function (first time) -aws lambda create-function \ - --function-name my-lambda-function \ - --runtime provided.al2023 \ - --role arn:aws:iam::ACCOUNT_ID:role/lambda-role \ - --handler bootstrap \ - --zip-file fileb://function.zip \ - --architectures x86_64 \ - --memory-size 512 \ - --timeout 30 - -# Update function code -aws lambda update-function-code \ - --function-name my-lambda-function \ - --zip-file fileb://function.zip -``` - ---- - -## Environment-Specific Configuration - -### Using appsettings.{Environment}.json - -```json title="appsettings.Production.json" -{ - "Logging": { - "LogLevel": { - "Default": "Information" - } - }, - "MyApp": { - "DatabaseConnectionString": "production-connection-string", - "CacheEnabled": true - } -} -``` - -**Set environment in Lambda**: - -```yaml title="template.yaml" -Environment: - Variables: - ASPNETCORE_ENVIRONMENT: Production -``` - -### Using AWS Systems Manager Parameter Store - -```csharp title="Program.cs" linenums="1" -using Amazon.Extensions.NETCore.Setup; -using Amazon.SimpleSystemsManagement; -using Amazon.SimpleSystemsManagement.Model; - -var builder = LambdaApplication.CreateBuilder(args); - -// Add AWS Systems Manager configuration -var ssmClient = new AmazonSimpleSystemsManagementClient(); -var parameter = await ssmClient.GetParameterAsync(new GetParameterRequest -{ - Name = "/myapp/database-connection-string", - WithDecryption = true -}); - -builder.Configuration.AddInMemoryCollection(new Dictionary -{ - ["ConnectionStrings:Default"] = parameter.Parameter.Value -}); - -var lambda = builder.Build(); -// ... -``` - ---- - -## Monitoring and Observability - -### CloudWatch Logs - -Lambda automatically sends logs to CloudWatch Logs. View logs: - -```bash -# Tail logs in real-time -sam logs --tail --name MyFunction - -# Or with AWS CLI -aws logs tail /aws/lambda/my-lambda-function --follow -``` - -### CloudWatch Metrics - -Monitor Lambda performance: - -```yaml title="template.yaml (SAM)" -Resources: - MyFunction: - Type: AWS::Serverless::Function - Properties: - # ... other properties - - # Alarm for errors - ErrorAlarm: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmName: !Sub "${MyFunction}-errors" - MetricName: Errors - Namespace: AWS/Lambda - Statistic: Sum - Period: 300 - EvaluationPeriods: 1 - Threshold: 5 - ComparisonOperator: GreaterThanThreshold - Dimensions: - - Name: FunctionName - Value: !Ref MyFunction -``` - -### OpenTelemetry Integration - -```bash -dotnet add package AwsLambda.Host.OpenTelemetry -``` - -```csharp title="Program.cs" linenums="1" -using AwsLambda.Host.OpenTelemetry; - -var builder = LambdaApplication.CreateBuilder(args); - -// Add OpenTelemetry tracing -builder.Services.AddLambdaOpenTelemetry(); - -var lambda = builder.Build(); -// ... -``` - ---- - -## Best Practices - -### ✅ Do - -- **Use Native AOT for production** - Faster cold starts and lower costs -- **Set appropriate memory and timeout** - Right-size based on profiling -- **Use environment variables for configuration** - Avoid hardcoding values -- **Implement health checks** - Monitor Lambda function health -- **Use blue/green deployments** - Zero-downtime deployments with SAM or CDK -- **Monitor with CloudWatch** - Set up alarms for errors and performance -- **Version your functions** - Use aliases for staged rollouts -- **Enable X-Ray tracing** - Understand performance bottlenecks -- **Use Parameter Store or Secrets Manager** - Secure configuration management -- **Automate deployments with CI/CD** - Consistent, repeatable deployments - -### ❌ Don't - -- **Don't hardcode secrets** - Use Secrets Manager or Parameter Store -- **Don't over-provision memory** - Start small and increase based on metrics -- **Don't deploy without testing** - Use sam local or integration tests -- **Don't ignore cold starts** - Use provisioned concurrency or AOT compilation -- **Don't skip monitoring** - Always configure CloudWatch alarms -- **Don't use default timeouts** - Tune based on actual execution time -- **Don't deploy directly to production** - Use staging environments - ---- - -## Versioning - -The current version of aws-lambda-host is **1.0.1-beta.5** (from `Directory.Build.props`). - -```xml -1.0.1-beta.5 -``` - -Update package references: - -```bash -dotnet add package AwsLambda.Host --version 1.0.1-beta.5 -``` - ---- - -## Key Takeaways - -1. **Native AOT recommended** - Use for fastest cold starts and lowest costs -2. **Multiple deployment options** - SAM for simplicity, CDK for type safety, Terraform for multi-cloud -3. **CI/CD essential** - Automate builds and deployments with GitHub Actions -4. **Right-size resources** - Profile and adjust memory/timeout settings -5. **Monitor everything** - CloudWatch Logs, Metrics, and X-Ray tracing -6. **Secure configuration** - Use Parameter Store or Secrets Manager for sensitive data -7. **Infrastructure as Code** - Never manually configure Lambda in the console -8. **Environment separation** - Use separate stacks for dev/staging/production - ---- - -## Next Steps - -- **[Configuration](configuration.md)** - Configure Lambda host options -- **[Error Handling](error-handling.md)** - Handle errors and configure DLQs -- **[Testing](testing.md)** - Test Lambda functions before deployment -- **[OpenTelemetry Package](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/AwsLambda.Host.OpenTelemetry)** - Add distributed tracing diff --git a/mkdocs.yml b/mkdocs.yml index 6f97c701..aabd8943 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -90,7 +90,6 @@ nav: - Installation: getting-started/installation.md - Your First Lambda: getting-started/first-lambda.md - Core Concepts: getting-started/core-concepts.md - - Project Structure: getting-started/project-structure.md - Guides: - guides/index.md - Dependency Injection: guides/dependency-injection.md @@ -100,7 +99,6 @@ nav: - Configuration: guides/configuration.md - Error Handling: guides/error-handling.md - Testing: guides/testing.md - - Deployment: guides/deployment.md - Features: - features/index.md - Envelopes: features/envelopes.md From d05493318a4e4bd7a2ab11f799e9da95d60b7cf9 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 10:33:37 -0500 Subject: [PATCH 36/65] feat(docs): simplify getting started guide with concise highlights - Rewrote introductory content for clarity and brevity, emphasizing framework benefits. - Consolidated "What You'll Learn" and "Framework Philosophy" into streamlined sections. - Removed repetitive examples and legacy descriptions for a cleaner guide. - Updated links to prioritize actionable next steps and highlight feature exploration paths. - Enhanced readability with concise bullet points and simplified structure. --- docs/getting-started/index.md | 199 ++++------------------------------ 1 file changed, 21 insertions(+), 178 deletions(-) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 3ca0eab5..bba5f016 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,51 +1,13 @@ # Getting Started -Welcome to **aws-lambda-host**, a modern .NET framework for building AWS Lambda functions using familiar .NET patterns and best practices. +**aws-lambda-host** brings ASP.NET Core–style hosting, dependency injection, and middleware to AWS Lambda. Instead of wiring up serialization and context handling manually, you configure a Lambda-specific host that manages scopes, middleware, and strongly typed handlers at compile time. -## What is aws-lambda-host? +### Why aws-lambda-host? -aws-lambda-host is a .NET hosting framework that brings the familiar patterns from ASP.NET Core to AWS Lambda development. Instead of writing boilerplate code to handle Lambda events, context, and serialization, you get a clean, declarative API for defining Lambda handlers with dependency injection, middleware support, and modern async/await patterns. - -Built on the generic host from Microsoft.Extensions, it provides a .NET hosting experience similar to ASP.NET Core but tailored specifically for Lambda execution. - -## Why Use This Framework? - -### Traditional Lambda Approach - -```csharp -public class Function -{ - public string FunctionHandler(string input, ILambdaContext context) - { - // Manual initialization - // Manual dependency management - // Manual error handling - return input.ToUpper(); - } -} -``` - -### aws-lambda-host Approach - -```csharp -var builder = LambdaApplication.CreateBuilder(); -builder.Services.AddScoped(); - -var lambda = builder.Build(); -lambda.MapHandler(([Event] string input, IMyService service) => - service.ProcessAsync(input)); - -await lambda.RunAsync(); -``` - -### Key Benefits - -- **Familiar Patterns** – Use the same .NET hosting patterns you know from ASP.NET Core -- **Dependency Injection** – Built-in DI container with proper scoped lifetime management -- **Middleware Pipeline** – Compose cross-cutting concerns like logging, validation, and error handling -- **Source Generation** – Compile-time code generation eliminates reflection overhead -- **AOT Ready** – Full support for Ahead-of-Time compilation for faster cold starts -- **Type Safety** – Strongly-typed event handlers with compile-time validation +- **Familiar patterns** – Builder APIs, DI, and middleware mirror ASP.NET Core. +- **Source-generated handlers** – Avoid reflection while staying AOT ready. +- **Lambda-focused lifecycle** – Startup, invocation, and shutdown hooks map to the Lambda runtime model. +- **Type-safe envelopes** – Request/response contracts are validated at compile time. ## Prerequisites @@ -61,147 +23,30 @@ Before you begin, ensure you have: - JetBrains Rider 2023.3+ - Visual Studio Code with C# Dev Kit -## What You'll Learn - -This Getting Started guide will walk you through everything you need to build production-ready Lambda functions: - -### 1. [Installation](installation.md) (~10 minutes) -- Install NuGet packages -- Configure your project file -- Verify your setup - -### 2. [Your First Lambda](first-lambda.md) (~20 minutes) -- Build a complete Lambda function step-by-step -- Add dependency injection -- Test locally -- Deploy to AWS - -### 3. [Core Concepts](core-concepts.md) (~30 minutes) -- Understand the Lambda lifecycle (OnInit, Invocation, OnShutdown) -- Master dependency injection patterns -- Work with middleware pipelines -- Learn about source generation - -### 4. [Project Structure](project-structure.md) (~15 minutes) -- Organize your Lambda projects -- Follow best practices -- Avoid common anti-patterns -- Structure for maintainability - -**Total Time**: ~85 minutes from zero to productive Lambda developer - -## Framework Philosophy - -aws-lambda-host is built on these core principles: - -### .NET Hosting Patterns - -Use the builder pattern, dependency injection, and middleware—the same patterns that make ASP.NET Core productive and maintainable. - -### Async-First Design - -Native support for async/await throughout the framework, with proper Lambda timeout and cancellation handling built in. - -### Source Generation Benefits - -Source generators analyze your handler code at compile time, generating optimized deserialization and DI resolution code. This eliminates reflection overhead and enables AOT compilation. +## Start Here -### Minimal Runtime Overhead +- **[Installation](installation.md)** – Add the NuGet packages and configure your csproj. +- **[Your First Lambda](first-lambda.md)** – Walk through a handler, DI setup, and local testing. +- **[Core Concepts](core-concepts.md)** – Learn about the host lifecycle, middleware, and source generation. -No unnecessary abstractions or reflection at runtime. The framework is designed for Lambda's constraints: fast cold starts, efficient memory usage, and predictable performance. +Prefer to explore? Head directly to **[Guides](/guides/)**, **[Examples](/examples/)**, or the **[API Reference](/api-reference/)** for deeper dives. -### Progressive Enhancement +## Framework Highlights -Start simple with basic handlers, then add middleware, observability, and advanced features as your needs grow. +- **Async-first runtime** – Cancellation tokens, timeout awareness, and scoped services work as expected. +- **Middleware pipeline** – Implement interception logic such as logging, validation, or OpenTelemetry spans in one place. +- **Envelope integrations** – Map SQS, SNS, API Gateway, and custom payloads using the envelope abstractions. +- **Observability-ready** – First-class OpenTelemetry integration for traces and metrics. -## Quick Example - -Here's a complete Lambda function that demonstrates the framework's key features: - -```csharp -using AwsLambda.Host; -using Microsoft.Extensions.DependencyInjection; - -// Define your models -public record OrderRequest(string OrderId, decimal Amount); -public record OrderResponse(bool Success, string Message); - -// Define your service -public interface IOrderService -{ - Task ProcessAsync(OrderRequest order); -} - -public class OrderService : IOrderService -{ - public async Task ProcessAsync(OrderRequest order) - { - // Business logic here - await Task.Delay(100); // Simulate processing - return new OrderResponse(true, $"Order {order.OrderId} processed"); - } -} - -// Configure and run -var builder = LambdaApplication.CreateBuilder(); -builder.Services.AddScoped(); - -var lambda = builder.Build(); - -// Add middleware for logging -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine($"Processing request at {DateTime.UtcNow}"); - await next(context); - Console.WriteLine($"Request completed at {DateTime.UtcNow}"); -}); - -// Register handler with dependency injection -lambda.MapHandler(async ([Event] OrderRequest request, IOrderService service) => - await service.ProcessAsync(request)); - -await lambda.RunAsync(); -``` - -This example shows: - -- ✅ Strongly-typed request/response models using records -- ✅ Service interface and implementation -- ✅ Dependency injection registration -- ✅ Middleware for cross-cutting concerns -- ✅ Handler with automatic DI resolution -- ✅ Async/await throughout - -## Next Steps - -Ready to start building? Choose your path: - -### For Beginners - -Follow the guide in order: - -1. **[Installation →](installation.md)** – Set up your environment -2. **[Your First Lambda →](first-lambda.md)** – Build a complete example -3. **[Core Concepts →](core-concepts.md)** – Understand the framework -4. **[Project Structure →](project-structure.md)** – Organize your code - -### For Experienced Developers - -Jump to what you need: - -- **[Installation](installation.md)** – Quick setup reference -- **[Core Concepts](core-concepts.md)** – Deep dive into architecture -- **[Guides](/guides/)** – Comprehensive feature documentation -- **[Examples](/examples/)** – Complete working examples -- **[API Reference](/api-reference/)** – Detailed API docs - -### Explore Features +## Explore Features - **[Envelopes](/features/envelopes/)** – Type-safe event source integration (SQS, SNS, API Gateway, etc.) -- **[OpenTelemetry](/features/opentelemetry.md)** – Distributed tracing and observability +- **[OpenTelemetry](/features/open_telemetry.md)** – Distributed tracing and observability - **[AOT Compilation](/advanced/aot-compilation.md)** – Optimize for fastest cold starts - **[Source Generators](/advanced/source-generators.md)** – Understand compile-time optimizations +## Need Help + ## Getting Help If you run into issues or have questions: @@ -211,6 +56,4 @@ If you run into issues or have questions: - **[GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues)** – Report bugs or request features - **[GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions)** – Ask questions and share ideas ---- - -Let's get started! Head to **[Installation →](installation.md)** to set up your environment. +Continue with **[Installation](installation.md)** to configure your project. From 4df2ff005d5d0b02d4e7959e4fc68659aeb26df0 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 10:46:30 -0500 Subject: [PATCH 37/65] feat(docs): improve installation guide formatting and update dependency versions - Updated installation table formatting for better alignment and readability. - Increased `AwsLambda.Host` package version to `1.2.1-beta.1`. - Removed outdated `InterceptorsNamespaces` configurations and warnings. - Added a tip linking to a working Lambda example for quick setup reference. - Clarified automatic source generator registration in the updated guide. --- docs/getting-started/installation.md | 47 +++++++++------------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index e72092ca..8d37a72d 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -6,12 +6,12 @@ This guide walks you through installing aws-lambda-host and configuring your pro Before you begin, ensure your development environment meets these requirements: -| Requirement | Minimum Version | Recommended | -|------------|----------------|-------------| -| .NET SDK | 8.0 | Latest LTS | -| C# Language Version | 11 | latest | -| IDE | Visual Studio 2022 (17.8+), Rider 2023.3+, or VS Code | Latest | -| AWS CLI | 2.0+ (optional) | Latest | +| Requirement | Minimum Version | Recommended | +|---------------------|-------------------------------------------------------|-------------| +| .NET SDK | 8.0 | Latest LTS | +| C# Language Version | 11 | latest | +| IDE | Visual Studio 2022 (17.8+), Rider 2023.3+, or VS Code | Latest | +| AWS CLI | 2.0+ (optional) | Latest | !!! note "C# 11 Requirement" C# 11 or later is required for source generators and interceptors that power the framework's compile-time optimizations. @@ -31,6 +31,8 @@ Choose your preferred installation method: dotnet add package AwsLambda.Host ``` + *Tip: the `examples/AwsLambda.Host.Example.HelloWorld` project in this repo shows a fully configured Lambda app if you prefer copying a working template.* + === "Visual Studio" 1. Right-click on your project in Solution Explorer @@ -44,7 +46,7 @@ Choose your preferred installation method: ```xml - + ``` @@ -80,20 +82,6 @@ Open your `.csproj` file and ensure these properties are set: ``` -### Source Generator Configuration - -Add this property to enable source generators and interceptors: - -```xml title="MyFirstLambda.csproj" - - - $(InterceptorsNamespaces);AwsLambda.Host - -``` - -!!! warning "InterceptorsNamespaces Required" - Without this setting, the framework's source generators won't work correctly and you'll get compilation errors. - ### Optional Settings (Recommended) These optional settings improve the Lambda development experience: @@ -124,17 +112,20 @@ Here's a complete, minimal `.csproj` file for a Lambda function: disable enable Lambda + true true true - $(InterceptorsNamespaces);AwsLambda.Host - + ``` +!!! info "Source Generators" + The `AwsLambda.Host` package ships an MSBuild target that automatically registers the required interceptor namespaces. No additional configuration is needed in your project file. + ## Verifying Installation Let's verify everything is set up correctly by creating a simple Lambda function. @@ -221,16 +212,6 @@ Envelope packages provide type-safe, strongly-typed event handling for specific **Solution**: Set `latest` in your `.csproj` file. -#### Source Generator Warning - -**Warning**: `Interceptor must be in a namespace annotated with 'InterceptsLocationAttribute'` - -**Solution**: Add the `InterceptorsNamespaces` property to your `.csproj`: - -```xml -$(InterceptorsNamespaces);AwsLambda.Host -``` - #### Build Errors After Installation **Error**: Various build errors after adding the package From c85d4a14dab631021e6a3db99b94aa433db1513e Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 10:47:17 -0500 Subject: [PATCH 38/65] chore(docs): fix formatting and restore stable dependency version - Adjusted table formatting for better alignment in installation guide. - Downgraded `AwsLambda.Host` dependency to stable version `1.2.0`. --- docs/getting-started/installation.md | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 8d37a72d..457aa97d 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -118,7 +118,7 @@ Here's a complete, minimal `.csproj` file for a Lambda function: - + ``` @@ -178,26 +178,26 @@ The aws-lambda-host framework includes multiple packages for different use cases ### Core Packages -| Package | Purpose | When to Use | -|---------|---------|-------------| -| **AwsLambda.Host** | Core framework | Required for all Lambda functions | -| **AwsLambda.Host.Abstractions** | Interfaces and contracts | When creating custom extensions or middleware | +| Package | Purpose | When to Use | +|----------------------------------|---------------------------|-----------------------------------------------| +| **AwsLambda.Host** | Core framework | Required for all Lambda functions | +| **AwsLambda.Host.Abstractions** | Interfaces and contracts | When creating custom extensions or middleware | | **AwsLambda.Host.OpenTelemetry** | Observability integration | When you need distributed tracing and metrics | ### Envelope Packages Envelope packages provide type-safe, strongly-typed event handling for specific AWS event sources: -| Package | Event Source | When to Use | -|---------|--------------|-------------| -| **AwsLambda.Host.Envelopes.Sqs** | Amazon SQS | Processing SQS queue messages | -| **AwsLambda.Host.Envelopes.Sns** | Amazon SNS | Handling SNS notifications | -| **AwsLambda.Host.Envelopes.ApiGateway** | API Gateway | Building REST/HTTP APIs | -| **AwsLambda.Host.Envelopes.Kinesis** | Kinesis Data Streams | Processing stream records | -| **AwsLambda.Host.Envelopes.KinesisFirehose** | Kinesis Firehose | Transforming Firehose data | -| **AwsLambda.Host.Envelopes.Kafka** | Apache Kafka / MSK | Processing Kafka messages | -| **AwsLambda.Host.Envelopes.CloudWatchLogs** | CloudWatch Logs | Processing log subscriptions | -| **AwsLambda.Host.Envelopes.Alb** | Application Load Balancer | ALB target Lambda functions | +| Package | Event Source | When to Use | +|----------------------------------------------|---------------------------|-------------------------------| +| **AwsLambda.Host.Envelopes.Sqs** | Amazon SQS | Processing SQS queue messages | +| **AwsLambda.Host.Envelopes.Sns** | Amazon SNS | Handling SNS notifications | +| **AwsLambda.Host.Envelopes.ApiGateway** | API Gateway | Building REST/HTTP APIs | +| **AwsLambda.Host.Envelopes.Kinesis** | Kinesis Data Streams | Processing stream records | +| **AwsLambda.Host.Envelopes.KinesisFirehose** | Kinesis Firehose | Transforming Firehose data | +| **AwsLambda.Host.Envelopes.Kafka** | Apache Kafka / MSK | Processing Kafka messages | +| **AwsLambda.Host.Envelopes.CloudWatchLogs** | CloudWatch Logs | Processing log subscriptions | +| **AwsLambda.Host.Envelopes.Alb** | Application Load Balancer | ALB target Lambda functions | !!! info "Envelope Packages" You only need envelope packages if you're working with those specific event sources. For simple use cases, just `AwsLambda.Host` is sufficient. Learn more in the [Envelopes documentation](/features/envelopes/). From fd4cc5f01962e68108ad78aef44a38ddd5fb7529 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 11:07:25 -0500 Subject: [PATCH 39/65] feat(docs): enhance getting started guide with detailed logging and middleware examples - Replaced `Console.WriteLine` usage with `ILogger` in logging middleware for better structured logs. - Added details on disabling Lambda output formatting for improved log payload consistency. - Updated examples with `ConfigureLambdaHostOptions` setup for local and production flexibility. - Streamlined program setup by consolidating redundant code and improving logging clarity. - Improved handler registration example with dependency injection usage and concise formatting. --- docs/getting-started/first-lambda.md | 130 ++++++++++++++++----------- 1 file changed, 78 insertions(+), 52 deletions(-) diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md index 2bb7008b..cb68b4bc 100644 --- a/docs/getting-started/first-lambda.md +++ b/docs/getting-started/first-lambda.md @@ -13,8 +13,6 @@ A greeting service Lambda function with: - ✅ Local testing - ✅ AWS deployment -**Time to Complete**: ~20 minutes - ## Prerequisites Before starting, ensure you've completed the [Installation](installation.md) guide and have: @@ -98,6 +96,7 @@ Set up the Lambda application builder and register your services with dependency using AwsLambda.Host.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; // Create the Lambda application builder var builder = LambdaApplication.CreateBuilder(); @@ -118,38 +117,54 @@ Add middleware to log when requests are received and completed. Middleware runs ```csharp title="Program.cs (continued)" // Add logging middleware -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request received"); - - // Execute the next middleware/handler in the pipeline - await next(context); +lambda.UseMiddleware( + async (context, next) => + { + var logger = context.ServiceProvider.GetRequiredService>(); - Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request completed"); -}); + logger.LogInformation("Request received at {Timestamp}", DateTime.UtcNow); + await next(context); + logger.LogInformation("Request completed at {Timestamp}", DateTime.UtcNow); + } +); ``` !!! tip "Middleware Use Cases" Middleware is perfect for cross-cutting concerns like logging, metrics, validation, error handling, and authentication. +!!! note "Clear Lambda Output Formatting" + The .NET Lambda runtime captures standard output/error and re-wraps every line into its own structured log record. + If you're running locally or relying on a custom logger (Serilog, NLog, etc.), disable that extra formatting so your + log payloads stay untouched: + + ```csharp title="Program.cs (continued)" + builder.Services.ConfigureLambdaHostOptions(options => + { + options.ClearLambdaOutputFormatting = true; + }); + ``` + + Prefer configuration files? Set `LambdaHostOptions:ClearLambdaOutputFormatting` to `true` in `appsettings.json` (or any + supported configuration source) for the same effect. See [Configuration → ClearLambdaOutputFormatting](../guides/configuration.md#clearlambdaoutputformatting) + for more details. + ## Step 6: Register the Handler Map your handler function with the `[Event]` attribute to mark the Lambda event parameter. ```csharp title="Program.cs (continued)" // Register the handler with dependency injection -lambda.MapHandler(([Event] GreetingRequest request, IGreetingService service) => -{ - // Call the service to get the greeting - var message = service.GetGreeting(request.Name, request.Language); - - // Return the response - return new GreetingResponse(message, DateTime.UtcNow); -}); +lambda.MapHandler( + ([Event] GreetingRequest request, IGreetingService service) => + { + var message = service.GetGreeting(request.Name, request.Language); + return new GreetingResponse(message, DateTime.UtcNow); + } +); ``` !!! warning "The [Event] Attribute" - The `[Event]` attribute is required on exactly one parameter. It tells the framework which parameter receives the deserialized Lambda event. + Add `[Event]` to at most one handler parameter to mark it as the deserialized Lambda event. If your handler does not accept an event payload (e.g., scheduled invocations or DI-only inputs), you can omit the attribute entirely. ## Step 7: Run the Lambda @@ -171,6 +186,43 @@ using System.Text.Json.Serialization; using AwsLambda.Host.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +// Lambda application setup +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddSingleton(); + +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.ClearLambdaOutputFormatting = true; +}); + +var lambda = builder.Build(); + +// Middleware +lambda.UseMiddleware( + async (context, next) => + { + var logger = context.ServiceProvider.GetRequiredService>(); + + logger.LogInformation("Request received at {Timestamp}", DateTime.UtcNow); + await next(context); + logger.LogInformation("Request completed at {Timestamp}", DateTime.UtcNow); + } +); + +// Handler +lambda.MapHandler( + ([Event] GreetingRequest request, IGreetingService service) => + { + var message = service.GetGreeting(request.Name, request.Language); + return new GreetingResponse(message, DateTime.UtcNow); + } +); + +// Run +await lambda.RunAsync(); // Request and Response models public record GreetingRequest( @@ -199,7 +251,7 @@ public class GreetingService : IGreetingService { "fr", "Bonjour" }, { "de", "Guten Tag" }, { "it", "Ciao" }, - { "ja", "こんにちは" } + { "ja", "こんにちは" }, }; public string GetGreeting(string name, string? language) @@ -208,30 +260,6 @@ public class GreetingService : IGreetingService return $"{greeting}, {name}!"; } } - -// Lambda application setup -var builder = LambdaApplication.CreateBuilder(); -builder.Services.AddSingleton(); - -var lambda = builder.Build(); - -// Middleware -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request received"); - await next(context); - Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request completed"); -}); - -// Handler -lambda.MapHandler(([Event] GreetingRequest request, IGreetingService service) => -{ - var message = service.GetGreeting(request.Name, request.Language); - return new GreetingResponse(message, DateTime.UtcNow); -}); - -// Run -await lambda.RunAsync(); ``` ## Testing Locally @@ -317,11 +345,13 @@ Try different payloads: ### Check the Logs -In the test tool output, you should see the middleware logs: +Because middleware uses `ILogger`, the Lambda Test Tool (or CloudWatch Logs) will include entries similar to: ``` -[2025-11-29 12:34:56] Request received -[2025-11-29 12:34:56] Request completed +info: GreetingService[0] + Request received at 2018-03-14T09:26:53Z +info: GreetingService[0] + Request completed at 2018-03-14T09:26:54Z ``` ## Deploying to AWS @@ -538,11 +568,7 @@ sequenceDiagram **Error**: `The [Event] attribute is not recognized` -**Solution**: Ensure you have the `InterceptorsNamespaces` configuration in your `.csproj`: - -```xml -$(InterceptorsNamespaces);AwsLambda.Host -``` +**Solution**: Add `using AwsLambda.Host.Builder;` to the top of your file (or fully qualify `[AwsLambda.Host.Builder.Event]`). The attribute ships with the `AwsLambda.Host` package—no additional project configuration is required. ### Timeout Errors From 31d1f404443ff932226134558bfb7a649fc4530d Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 11:21:44 -0500 Subject: [PATCH 40/65] feat(docs): improve Lambda startup and service lifetimes guide formatting - Added notes on placing supporting types at the bottom of `Program.cs` for better structure. - Improved OnInit, Invocation, and OnShutdown documentation with additional details and clarity. - Included examples for transient service registrations and use cases. - Enhanced DI scope visualization section with practical guidance on handler parameter usage. - Streamlined `[Event]` attribute explanation with improved examples and error case descriptions. --- docs/getting-started/core-concepts.md | 237 +++++++++++++++----------- docs/getting-started/first-lambda.md | 11 +- 2 files changed, 146 insertions(+), 102 deletions(-) diff --git a/docs/getting-started/core-concepts.md b/docs/getting-started/core-concepts.md index 2e3e9da1..63ea12c7 100644 --- a/docs/getting-started/core-concepts.md +++ b/docs/getting-started/core-concepts.md @@ -13,12 +13,14 @@ AWS Lambda functions go through three distinct phases during their execution. Un The OnInit phase runs **once** when a Lambda container starts (cold start). This is your opportunity to perform expensive setup operations that can be reused across multiple invocations. **Characteristics:** + - Runs only once per container lifecycle - Singleton services are created during this phase - Exceptions here prevent the Lambda from starting - Should complete quickly to minimize cold start time **Perfect for:** + - Opening database connections - Loading configuration - Warming caches @@ -42,6 +44,7 @@ lambda.OnInit(async (services, cancellationToken) => The Invocation phase runs **for each Lambda event**. This is where your business logic executes. **Characteristics:** + - Runs for every invocation - New DI scope created per invocation - Scoped services are instantiated @@ -49,6 +52,7 @@ The Invocation phase runs **for each Lambda event**. This is where your business - Exceptions are returned as error responses **What happens:** + 1. Lambda Runtime receives event 2. Event is deserialized to your model 3. New DI scope created @@ -62,12 +66,16 @@ The Invocation phase runs **for each Lambda event**. This is where your business The OnShutdown phase runs **once** before the Lambda container terminates. Use this for cleanup and flushing data. **Characteristics:** + - Runs once before container shutdown -- Limited time window (configurable, default 2 seconds) +- Limited time window (configurable, default 500ms with a 50ms safety buffer) - Perfect for flushing metrics, logs, or buffers - Services are still available for injection +By default `LambdaHostOptions` sets `ShutdownDuration` to `ShutdownDuration.ExternalExtensions` (500ms) and subtracts `ShutdownDurationBuffer` (50ms). Increase those values if your cleanup needs a longer runway. + **Perfect for:** + - Flushing telemetry data - Closing database connections - Final cleanup operations @@ -105,9 +113,9 @@ graph LR ```csharp using System; -using System.Threading.Tasks; -using AwsLambda.Host; +using AwsLambda.Host.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; var builder = LambdaApplication.CreateBuilder(); @@ -162,7 +170,7 @@ Dependency injection (DI) is a core feature of aws-lambda-host. It enables testa ### Service Lifetimes -The framework supports two service lifetimes, matching Lambda's execution model: +`aws-lambda-host` uses the standard `Microsoft.Extensions.DependencyInjection` container, so you can register **singleton**, **scoped**, and **transient** services. Lambda's execution model makes singleton and scoped the most common choices, but transients work too when you need a fresh instance every time a dependency is resolved. #### Singleton (Container Level) @@ -184,7 +192,7 @@ Singleton services are created **once** during the OnInit phase and **reused acr **Example:** ```csharp -builder.Services.AddSingleton(); +builder.Services.AddHttpClient(); // registers IHttpClientFactory + typed clients builder.Services.AddSingleton(); builder.Services.AddSingleton(builder.Configuration); ``` @@ -217,6 +225,26 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); ``` +#### Transient (Per Resolve) + +Transient services are created **every time** they are requested from the container. + +**Characteristics:** +- New instance per resolution (even within the same invocation) +- Great for lightweight, stateless helpers +- Avoid expensive setup logic here—prefer scoped/singleton for that + +**Use carefully for:** +- Simple mappers/formatters +- Stateless validators +- Small wrappers around third-party SDK calls + +Register them the same way you would in ASP.NET Core: + +```csharp +builder.Services.AddTransient(); +``` + ### DI Scope Visualization ```mermaid @@ -241,6 +269,8 @@ Handlers can inject various types of parameters: | `ILambdaHostContext` | Framework context with Lambda metadata | No | | `CancellationToken` | For cancellation and timeout handling | No | +If your handler doesn't need an event payload, simply leave the `[Event]` row out of the parameter list and inject only the services you need. + **Example:** ```csharp @@ -276,7 +306,7 @@ lambda.MapHandler(async ( var builder = LambdaApplication.CreateBuilder(); // === Singleton Services (shared) === -builder.Services.AddSingleton(); +builder.Services.AddHttpClient(); builder.Services.AddSingleton(); // Singleton with factory @@ -529,24 +559,24 @@ lambda.MapHandler(([Event] Request request, ILambdaHostContext context) => ### The [Event] Attribute -The `[Event]` attribute is **required** on exactly one parameter to mark the Lambda event. +Use `[Event]` to tell the source generator which parameter should receive the deserialized Lambda payload. Handlers can have zero or one `[Event]` parameters: -**Rules:** -- Must be used on exactly one parameter -- Marks the parameter that receives the deserialized Lambda event -- Triggers source generation for type-safe deserialization -- Enables compile-time validation +- ✅ Mark exactly one parameter if you want the framework to deserialize the incoming event. +- ✅ Omit the attribute entirely for DI-only handlers (scheduled jobs, Queue pollers, etc.). The parameter list can consist solely of injected services. +- ❌ Do not decorate multiple parameters—the generator emits `LH0002` if you try. -**Example:** +If you accidentally leave `[Event]` off a parameter that should receive the payload, the framework assumes it's a DI service and no event data is bound, so you'll spot the issue quickly during testing. + +**Examples:** ```csharp -// ✅ CORRECT: One [Event] parameter -lambda.MapHandler(([Event] Order order, IService service) => { ... }); +// Typical case: bind the event payload +lambda.MapHandler(([Event] Order order, IService service) => service.Process(order)); -// ❌ WRONG: No [Event] parameter -lambda.MapHandler((Order order, IService service) => { ... }); +// DI-only handler: no event payload required +lambda.MapHandler((IMaintenanceJob job, ILogger logger) => job.RunAsync(logger)); -// ❌ WRONG: Multiple [Event] parameters +// Compile-time error: multiple [Event] attributes lambda.MapHandler(([Event] Order order, [Event] string id) => { ... }); ``` @@ -642,108 +672,123 @@ using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using AwsLambda.Host; +using AwsLambda.Host.Builder; +using AwsLambda.Host.Core; using Microsoft.Extensions.DependencyInjection; - -// === MODELS === -public record Order(string Id, decimal Amount); -public record OrderResult(string OrderId, bool Success); - -// === SERVICES === -public interface ICache -{ - Task WarmupAsync(CancellationToken ct); - bool TryGet(string key, out OrderResult value); - void Set(string key, OrderResult value); -} - -public interface IOrderRepository -{ - Task ProcessAsync(Order order, CancellationToken ct); -} +using Microsoft.Extensions.Hosting; // === SETUP === var builder = LambdaApplication.CreateBuilder(); // Register services with appropriate lifetimes -builder.Services.AddSingleton(); // Shared across invocations -builder.Services.AddScoped(); // Per invocation +builder.Services.AddSingleton(); // Shared across invocations +builder.Services.AddScoped(); // Per invocation var lambda = builder.Build(); // === LIFECYCLE: OnInit (runs once) === -lambda.OnInit(async (services, cancellationToken) => -{ - Console.WriteLine("=== Initializing Lambda ==="); - var cache = services.GetRequiredService(); - await cache.WarmupAsync(cancellationToken); - Console.WriteLine("=== Initialization Complete ==="); - return true; -}); +lambda.OnInit( + async (services, cancellationToken) => + { + Console.WriteLine("=== Initializing Lambda ==="); + var cache = services.GetRequiredService(); + await cache.WarmupAsync(cancellationToken); + Console.WriteLine("=== Initialization Complete ==="); + return true; + } +); // === MIDDLEWARE 1: Logging === -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine($"[{DateTime.UtcNow}] Request started"); - await next(context); - Console.WriteLine($"[{DateTime.UtcNow}] Request completed"); -}); +lambda.UseMiddleware( + async (context, next) => + { + Console.WriteLine($"[{DateTime.UtcNow}] Request started"); + await next(context); + Console.WriteLine($"[{DateTime.UtcNow}] Request completed"); + } +); // === MIDDLEWARE 2: Timing === -lambda.UseMiddleware(async (context, next) => -{ - var stopwatch = Stopwatch.StartNew(); - await next(context); - stopwatch.Stop(); - Console.WriteLine($"Duration: {stopwatch.ElapsedMilliseconds}ms"); -}); +lambda.UseMiddleware( + async (context, next) => + { + var stopwatch = Stopwatch.StartNew(); + await next(context); + stopwatch.Stop(); + Console.WriteLine($"Duration: {stopwatch.ElapsedMilliseconds}ms"); + } +); // === MIDDLEWARE 3: Correlation ID === -lambda.UseMiddleware(async (context, next) => -{ - var correlationId = Guid.NewGuid().ToString(); - context.Items["CorrelationId"] = correlationId; - Console.WriteLine($"Correlation ID: {correlationId}"); - await next(context); -}); +lambda.UseMiddleware( + async (context, next) => + { + var correlationId = Guid.NewGuid().ToString(); + context.Items["CorrelationId"] = correlationId; + Console.WriteLine($"Correlation ID: {correlationId}"); + await next(context); + } +); // === HANDLER: Business Logic with DI === -lambda.MapHandler(async ( - [Event] Order order, // Lambda event - IOrderRepository repository, // Scoped service - ICache cache, // Singleton service - ILambdaHostContext context, // Framework context - CancellationToken cancellationToken // Cancellation token -) => -{ - // Check cache first - if (cache.TryGet(order.Id, out var cached)) +lambda.MapHandler( + async ( + [Event] Order order, // Lambda event + IOrderRepository repository, // Scoped service + ICache cache, // Singleton service + ILambdaHostContext context, // Framework context + CancellationToken cancellationToken // Cancellation token + ) => { - Console.WriteLine($"Cache hit for order {order.Id}"); - return cached; - } + // Check cache first + if (cache.TryGet(order.Id, out var cached)) + { + Console.WriteLine($"Cache hit for order {order.Id}"); + return cached; + } - // Process order - Console.WriteLine($"Processing order {order.Id}"); - var result = await repository.ProcessAsync(order, cancellationToken); + // Process order + Console.WriteLine($"Processing order {order.Id}"); + var result = await repository.ProcessAsync(order, cancellationToken); - // Update cache - cache.Set(order.Id, result); + // Update cache + cache.Set(order.Id, result); - return result; -}); + return result; + } +); // === LIFECYCLE: OnShutdown (runs once) === -lambda.OnShutdown(async (services, cancellationToken) => -{ - Console.WriteLine("=== Shutting Down Lambda ==="); - var cache = services.GetRequiredService(); - // Flush cache or perform cleanup - Console.WriteLine("=== Shutdown Complete ==="); -}); +lambda.OnShutdown( + async (services, cancellationToken) => + { + Console.WriteLine("=== Shutting Down Lambda ==="); + var cache = services.GetRequiredService(); + // Flush cache or perform cleanup + Console.WriteLine("=== Shutdown Complete ==="); + } +); // === RUN === await lambda.RunAsync(); + +// === MODELS === +public record Order(string Id, decimal Amount); + +public record OrderResult(string OrderId, bool Success); + +// === SERVICES === +public interface ICache +{ + Task WarmupAsync(CancellationToken ct); + bool TryGet(string key, out OrderResult value); + void Set(string key, OrderResult value); +} + +public interface IOrderRepository +{ + Task ProcessAsync(Order order, CancellationToken ct); +} ``` ## Key Takeaways @@ -775,12 +820,6 @@ Now you understand the core concepts of aws-lambda-host: - AOT compilation ready - Compile-time validation -## Next Steps - -Now that you understand the core concepts, learn how to organize your Lambda projects effectively: - -**→ [Project Structure](project-structure.md)** – Learn best practices for organizing your code, managing configuration, and structuring tests. - ### Explore Further - **[Middleware Guide](/guides/middleware.md)** – Advanced middleware patterns diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md index cb68b4bc..2bf3dbb9 100644 --- a/docs/getting-started/first-lambda.md +++ b/docs/getting-started/first-lambda.md @@ -25,7 +25,11 @@ Before starting, ensure you've completed the [Installation](installation.md) gui Create strongly-typed models for your Lambda's input and output using C# records. -Add this code to your `Program.cs`: +!!! note "Keep supporting types at the bottom" + Place records, interfaces, and service implementations **after** the main pipeline (after `await lambda.RunAsync();`). + This keeps startup logic together at the top while still shipping everything as a single file. + +Add this code to the end of your `Program.cs`: ```csharp title="Program.cs" linenums="1" using System; @@ -52,7 +56,8 @@ public record GreetingResponse( ## Step 2: Create a Service Interface -Define an interface for your business logic. This enables dependency injection and testability. +Define an interface for your business logic. This enables dependency injection and testability. Keep +this with your other supporting types at the bottom of `Program.cs`. ```csharp title="Program.cs (continued)" // Service interface @@ -64,7 +69,7 @@ public interface IGreetingService ## Step 3: Implement the Service -Implement your service with the actual greeting logic. +Implement your service with the actual greeting logic (also placed after the main pipeline). ```csharp title="Program.cs (continued)" // Service implementation From 47e6d17321a09b1f713040de470cc867431a6bc8 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 11:30:18 -0500 Subject: [PATCH 41/65] feat(docs): remove outdated deployment guide and add examples section - Deleted `Deployment` section from `index.md` to streamline documentation. - Updated `mkdocs.yml` to reflect removal of deployment guide. - Added a new `Examples --- docs/guides/index.md | 11 ----------- mkdocs.yml | 6 +++++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/docs/guides/index.md b/docs/guides/index.md index 6abe3a84..d447ff03 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -84,17 +84,6 @@ Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, an - Integration testing - Test naming conventions -### [Deployment](deployment.md) -Deploy Lambda functions using AWS SAM, CDK, Terraform, or CI/CD pipelines. - -**Topics covered:** -- Project configuration (standard and AOT) -- AWS SAM deployment -- AWS CDK deployment -- CI/CD with GitHub Actions -- Versioning and releases -- Best practices - ## Guide Features Each guide includes: diff --git a/mkdocs.yml b/mkdocs.yml index aabd8943..b97c025d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,13 +99,17 @@ nav: - Configuration: guides/configuration.md - Error Handling: guides/error-handling.md - Testing: guides/testing.md + - Examples: + - examples/index.md - Features: - features/index.md - Envelopes: features/envelopes.md - OpenTelemetry: features/open_telemetry.md + - Advanced: + - advanced/index.md plugins: - search extra_css: - - stylesheets/extra.css \ No newline at end of file + - stylesheets/extra.css From a0aa188a145b53e4c59b7760239128a1b41b3c84 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 11:32:50 -0500 Subject: [PATCH 42/65] feat(docs): add placeholders for advanced topics and enhance examples section - Created `Advanced Topics` with placeholders for Native AOT, performance tuning, and source generators. - Enhanced `Examples` section with notes on runnable Lambda applications and middleware usage. - Updated `Guides` formatting by adding spacing for improved readability. - Refined guide links in `Additional Resources` to reflect upcoming content and placeholders. --- docs/advanced/index.md | 5 +++++ docs/examples/index.md | 5 +++++ docs/guides/index.md | 26 +++++++++----------------- 3 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 docs/advanced/index.md create mode 100644 docs/examples/index.md diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 00000000..793b3ec5 --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,5 @@ +# Advanced Topics + +> Coming soon: guidance on Native AOT, source generators internals, and performance tuning for aws-lambda-host. + +We're working on detailed content for advanced scenarios. In the meantime, follow the repository's CHANGELOG and GitHub discussions for early tips. diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 00000000..3e8e48a3 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,5 @@ +# Examples + +> Full walkthrough examples are on the way. You'll soon find runnable Lambda applications that demonstrate middleware, envelopes, and DI patterns. + +Until then, explore the `examples/` folder in the repository for the latest sample projects. diff --git a/docs/guides/index.md b/docs/guides/index.md index d447ff03..7a943d49 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -10,6 +10,7 @@ Master the essential framework features that power your Lambda functions. Learn service registration patterns, understand Singleton vs Scoped lifetimes, and master dependency injection in handlers and lifecycle methods. **Topics covered:** + - Service lifetime management (Singleton, Scoped) - Service registration patterns - Injectable parameter types @@ -30,6 +31,7 @@ Build middleware pipelines for cross-cutting concerns like logging, metrics, val Understand and control the Lambda lifecycle phases: OnInit, Invocation, and OnShutdown. **Topics covered:** + - OnInit phase for cold start setup - OnShutdown phase for cleanup - Multiple lifecycle handlers @@ -40,6 +42,7 @@ Understand and control the Lambda lifecycle phases: OnInit, Invocation, and OnSh Register type-safe Lambda handlers with automatic dependency injection and source generation. **Topics covered:** + - MapHandler method usage - `[Event]` attribute requirements - Injectable parameter types @@ -51,6 +54,7 @@ Register type-safe Lambda handlers with automatic dependency injection and sourc Configure framework behavior with LambdaHostOptions and application settings. **Topics covered:** + - LambdaHostOptions reference - Timeout and cancellation configuration - Application configuration patterns @@ -66,6 +70,7 @@ Build robust, testable, and deployable Lambda functions. Implement resilient error handling with retries, graceful degradation, and proper exception management. **Topics covered:** + - Exception handling in handlers - Middleware error handling - Cancellation token usage @@ -77,6 +82,7 @@ Implement resilient error handling with retries, graceful degradation, and prope Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, and AutoFixture. **Topics covered:** + - Testing framework setup - Unit testing services - AutoNSubstituteData pattern @@ -84,16 +90,6 @@ Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, an - Integration testing - Test naming conventions -## Guide Features - -Each guide includes: - -- ✅ **Complete working examples** – Copy-paste ready code -- ✅ **Best practices** – Proven patterns from production use -- ✅ **Anti-patterns** – Common mistakes to avoid -- ✅ **Troubleshooting** – Solutions to common issues -- ✅ **Cross-references** – Links to related topics - ## Learning Path ### New to aws-lambda-host? @@ -110,18 +106,14 @@ After mastering the guides, explore [Advanced Topics](/advanced/) for AOT compil ## Additional Resources -- **[Examples](/examples/)** – Complete example projects -- **[Features](/features/)** – Envelope packages and OpenTelemetry -- **[API Reference](/api-reference/)** – Detailed API documentation -- **[FAQ](/resources/faq.md)** – Common questions and answers -- **[Troubleshooting](/resources/troubleshooting.md)** – Solutions to common problems +- **[Examples](/examples/)** – Repository sample projects (more coming soon) +- **[Features](/features/)** – Envelope packages and OpenTelemetry add-ons +- **[Advanced Topics](/advanced/)** – Placeholder for Native AOT, generators, and performance deep dives ## Getting Help If you encounter issues not covered in these guides: -- Check the [FAQ](/resources/faq.md) for common questions -- Review [Troubleshooting](/resources/troubleshooting.md) for solutions - Search or ask in [GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions) - Report bugs in [GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues) From 39846de6e2ef46f679cfe1ddcfdc25acf3c2bd15 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 11:55:30 -0500 Subject: [PATCH 43/65] feat(docs): streamline dependency injection guide - Replaced lengthy sections with a concise explanation of DI concepts in Lambda functions. - Simplified `Service Lifetimes` and removed redundant examples for better focus. - Improved formatting and added practical usage tips for scoped, singleton, and transient services. - Enhanced `OnInit` and `OnShutdown` sections with streamlined examples and guidance. - Updated DI container creation example to prioritize core concepts and improved readability. --- docs/guides/dependency-injection.md | 923 +++------------------------- 1 file changed, 88 insertions(+), 835 deletions(-) diff --git a/docs/guides/dependency-injection.md b/docs/guides/dependency-injection.md index d99d5034..68319f7c 100644 --- a/docs/guides/dependency-injection.md +++ b/docs/guides/dependency-injection.md @@ -1,897 +1,150 @@ # Dependency Injection -Dependency injection (DI) is central to building maintainable Lambda functions with aws-lambda-host. The framework integrates Microsoft.Extensions.DependencyInjection, providing the same DI patterns you use in ASP.NET Core—but optimized for Lambda's unique execution model. +`aws-lambda-host` uses the same dependency injection container as ASP.NET Core +(`Microsoft.Extensions.DependencyInjection`). If you're new to DI in .NET, start with the +[official documentation](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) +and then come back for Lambda-specific guidance. This guide focuses on what changes (and what stays the +same) when you run inside AWS Lambda. -## Introduction - -In Lambda functions, understanding service lifetimes is critical: - -- **Singleton services** persist across invocations (cold start to shutdown) -- **Scoped services** are created fresh for each invocation -- **Transient services** are created each time they're requested - -This guide teaches you how to leverage DI effectively in Lambda functions. - -## Service Lifetimes - -### Singleton Services - -Singleton services are created once during the first invocation and reused for the lifetime of the Lambda execution environment. - -**When to use:** -- Stateless services -- HTTP clients (with connection pooling) -- Cache instances -- Configuration objects -- Database connection pools +## How the Container Is Created ```csharp title="Program.cs" -using AwsLambda.Host; -using Microsoft.Extensions.DependencyInjection; - var builder = LambdaApplication.CreateBuilder(); -// Singleton: Created once, reused across all invocations builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -var lambda = builder.Build(); -``` - -**Benefits:** -- ✅ Optimal performance (no repeated initialization) -- ✅ Shared state across invocations (caching, connection pools) -- ✅ Lower memory usage - -**⚠️ Warning:** Singleton services must be thread-safe since Lambda can process concurrent invocations in the same execution environment. - -### Scoped Services - -Scoped services are created once per Lambda invocation and disposed when the invocation completes. - -**When to use:** -- Services with per-request state -- Database contexts (Entity Framework) -- Unit of Work patterns -- Request-specific logging contexts -- Services that shouldn't be shared between requests - -```csharp title="Program.cs" -using AwsLambda.Host; -using Microsoft.Extensions.DependencyInjection; - -var builder = LambdaApplication.CreateBuilder(); - -// Scoped: New instance per invocation builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddTransient, OrderValidator>(); var lambda = builder.Build(); ``` -**Benefits:** -- ✅ Isolated per invocation (no cross-request contamination) -- ✅ Automatic disposal after each request -- ✅ Safe for services with mutable state - -### Transient Services - -Transient services are created each time they're requested from the service provider. - -**When to use:** -- Lightweight, stateless services -- Services that need fresh instances each time -- Rarely needed in Lambda (prefer Scoped for most use cases) - -```csharp title="Program.cs" -builder.Services.AddTransient(); -``` - -**⚠️ Note:** Transient services are rarely necessary in Lambda. Prefer Scoped for per-invocation services and Singleton for shared services. - -## Service Registration Patterns - -### Basic Registration - -Register interface/implementation pairs: - -```csharp title="Program.cs" -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -builder.Services.AddTransient(); -``` - -### Concrete Types - -Register concrete types without interfaces: - -```csharp title="Program.cs" -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -``` - -### Factory Registration - -Use factories for complex initialization: - -```csharp title="Program.cs" -builder.Services.AddSingleton(sp => -{ - var logger = sp.GetRequiredService>(); - return new MemoryCache( - new MemoryCacheOptions - { - SizeLimit = 1024, - CompactionPercentage = 0.25 - }, - logger - ); -}); -``` - -### Multiple Implementations - -Register multiple implementations of the same interface: - -```csharp title="Program.cs" -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -// Inject all implementations -lambda.MapHandler(([Event] Order order, IEnumerable notifiers) => -{ - foreach (var notifier in notifiers) - { - notifier.Notify(order); - } -}); -``` - -### Try Add - -Register services only if not already registered: - -```csharp title="Program.cs" -builder.Services.TryAddSingleton(); -// If ICache is already registered, this does nothing -``` - -### Replace Services - -Replace existing service registrations: - -```csharp title="Program.cs" -builder.Services.Replace( - ServiceDescriptor.Singleton() -); -``` - -## Dependency Injection in Handlers - -Handlers can inject any registered service, plus framework-provided types. - -### Injectable Types - -The framework automatically injects these types into handlers: - -| Type | Description | Lifetime | -|------|-------------|----------| -| `[Event] T` | The Lambda event payload (deserialized) | Per invocation | -| `IServiceType` | Any registered service | As registered | -| `ILambdaHostContext` | Lambda invocation context | Per invocation | -| `CancellationToken` | Cancellation signal (timeout tracking) | Per invocation | - -### Basic Handler Injection - -```csharp title="Program.cs" -lambda.MapHandler(([Event] OrderRequest request, IOrderService service) => - service.ProcessAsync(request) -); -``` - -### Multiple Dependencies - -Inject multiple services into handlers: - -```csharp title="Program.cs" -lambda.MapHandler(async ( - [Event] OrderRequest request, - IOrderService orderService, - IInventoryService inventoryService, - IPaymentService paymentService, - ILogger logger -) => -{ - logger.LogInformation("Processing order {OrderId}", request.OrderId); - - var inventoryAvailable = await inventoryService.CheckAsync(request.Items); - if (!inventoryAvailable) - { - return new OrderResponse { Success = false, Reason = "Inventory unavailable" }; - } - - var paymentResult = await paymentService.ChargeAsync(request.Payment); - if (!paymentResult.Success) - { - return new OrderResponse { Success = false, Reason = "Payment failed" }; - } - - var orderResult = await orderService.CreateAsync(request); - return new OrderResponse { Success = true, OrderId = orderResult.OrderId }; -}); -``` - -### Injecting Context +- `builder.Services` is the same `IServiceCollection` you use everywhere else in .NET. +- All registrations must happen **before** `builder.Build()`. +- Keep supporting types (records, services, options classes) at the bottom of `Program.cs`; keep the + pipeline (DI, middleware, handlers, run) at the top so cold-start work stays easy to read. -Use `ILambdaHostContext` to access invocation metadata: +## Service Lifetimes in Lambda -```csharp title="Program.cs" -lambda.MapHandler(async ( - [Event] OrderRequest request, - IOrderService service, - ILambdaHostContext context -) => -{ - // Access scoped service provider - var cache = context.ServiceProvider.GetRequiredService(); +Lambda containers live across multiple invocations. Map the standard lifetimes to Lambda's lifecycle: - // Access cancellation token - if (context.CancellationToken.IsCancellationRequested) - { - return new OrderResponse { Success = false, Reason = "Timeout" }; - } +| Lifetime | When it's created | When it's disposed | Use for | +|-----------|----------------------------------|-------------------------------------------|--------------------------------------------| +| Singleton | During OnInit (first cold start) | When the execution environment shuts down | HttpClient, caches, telemetry, config | +| Scoped | Once per invocation | After the invocation completes | DbContext, repositories, per-request state | +| Transient | Every time it's requested | After the requesting scope is disposed | Lightweight helpers, pure functions | - // Store invocation-scoped data - context.Items["StartTime"] = DateTimeOffset.UtcNow; +**Tips:** - return await service.ProcessAsync(request, context.CancellationToken); -}); -``` +- Scoped services are the default choice for anything that shouldn't leak state between invocations. +- Transients work the same as in ASP.NET Core, but prefer Scoped unless you truly need a new instance + every time a constructor runs. -### Injecting CancellationToken +## Invocation Scope and `ILambdaHostContext` -Use `CancellationToken` to handle Lambda timeouts gracefully: +Every invocation gets its own scope. You can access it via the `ILambdaHostContext` and it is shared +across middleware and handlers: -```csharp title="Program.cs" +```csharp title="Handlers" lambda.MapHandler(async ( [Event] OrderRequest request, - IOrderService service, - CancellationToken cancellationToken + IOrderService orders, // scoped service + ILambdaHostContext context, // framework context + CancellationToken cancellation // host-managed token ) => { - try - { - return await service.ProcessAsync(request, cancellationToken); - } - catch (OperationCanceledException) - { - // Lambda is approaching timeout - return new OrderResponse { Success = false, Reason = "Timeout" }; - } + context.Items["RequestId"] = request.Id; + return await orders.ProcessAsync(request, cancellation); }); ``` -## Dependency Injection in Middleware - -Middleware can resolve services from the DI container using the `ILambdaHostContext`. - -### Service Resolution in Middleware - -```csharp title="Program.cs" -lambda.UseMiddleware(async (context, next) => -{ - var logger = context.ServiceProvider.GetRequiredService>(); - var stopwatch = Stopwatch.StartNew(); +`ILambdaHostContext` exposes: - logger.LogInformation("Request starting"); +- `ServiceProvider` – the scoped service provider for the invocation +- `CancellationToken` – automatically linked to Lambda remaining time +- `Items` – per-invocation storage shared by middleware/handlers +- `Properties` – cross-invocation storage backed by the singleton container +- `Features` – typed feature collections (advanced scenarios) - await next(context); +If your handler doesn't need the Lambda payload, omit the `[Event]` parameter entirely and inject only services. - stopwatch.Stop(); - logger.LogInformation("Request completed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); -}); -``` +!!! tip "Cancellation buffers" + The cancellation token fires slightly **before** AWS kills the process: -### Middleware with Constructor Injection + - The runtime subtracts `LambdaHostOptions.InvocationCancellationBuffer` (default 3s) from the + remaining time when creating the token. + - Always pass it down to outbound SDK calls and database queries so you can stop work cleanly. -Create reusable middleware classes with constructor injection: +## Middleware and Lifecycle Hooks -```csharp title="Middleware/LoggingMiddleware.cs" -using AwsLambda.Host.Abstractions; -using Microsoft.Extensions.Logging; +- Middleware receives the invocation scope via the `ILambdaHostContext` argument. Resolve services with + `context.ServiceProvider` or create reusable middleware classes with constructor injection. +- `OnInit` and `OnShutdown` handlers run outside the normal invocation scope but each one executes + inside its own scoped service provider so you can warm caches, seed connections, or flush telemetry + without leaking per-invocation services. -public class LoggingMiddleware -{ - private readonly ILogger _logger; - - public LoggingMiddleware(ILogger logger) - { - _logger = logger; - } - - public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) - { - _logger.LogInformation("Starting invocation"); - await next(context); - _logger.LogInformation("Invocation completed"); - } -} -``` - -```csharp title="Program.cs" -builder.Services.AddSingleton(); - -var lambda = builder.Build(); -lambda.UseMiddleware(); -``` - -## Dependency Injection in Lifecycle Hooks - -Both `OnInit` and `OnShutdown` handlers support dependency injection. - -### OnInit with DI - -```csharp title="Program.cs" -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +```csharp title="OnInit" +lambda.OnInit(async (services, ct) => { var cache = services.GetRequiredService(); - var logger = services.GetRequiredService>(); - - logger.LogInformation("Warming up cache"); await cache.WarmUpAsync(ct); - - return true; // Continue initialization -}); -``` - -### OnShutdown with DI - -```csharp title="Program.cs" -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => -{ - var cache = services.GetRequiredService(); - var logger = services.GetRequiredService>(); - - logger.LogInformation("Flushing cache"); - await cache.FlushAsync(ct); + return true; }); ``` -## Configuration with Options Pattern - -Use the options pattern to inject strongly-typed configuration. - -### Defining Options Classes +## Configuration and Options -```csharp title="Configuration/OrderProcessingOptions.cs" -namespace MyLambda.Configuration; - -public class OrderProcessingOptions -{ - public int MaxRetries { get; init; } - public int TimeoutSeconds { get; init; } - public bool EnableCaching { get; init; } -} -``` - -### Binding Configuration +Use the standard options pattern for configuration: ```csharp title="Program.cs" -using Microsoft.Extensions.Options; - -var builder = LambdaApplication.CreateBuilder(); - -// Bind configuration section to options class builder.Services.Configure( - builder.Configuration.GetSection("OrderProcessing") -); - -builder.Services.AddScoped(); - -var lambda = builder.Build(); -``` - -### appsettings.json - -```json title="appsettings.json" -{ - "OrderProcessing": { - "MaxRetries": 3, - "TimeoutSeconds": 30, - "EnableCaching": true - } -} + builder.Configuration.GetSection("OrderProcessing")); ``` -### Injecting Options +Prefer `IOptions` inside handlers/services so the value is captured once per cold start. Snapshot/Monitor +variants work but rarely matter in Lambda because configuration usually ships with the deployment package. -```csharp title="Services/OrderService.cs" -using Microsoft.Extensions.Options; +## Patterns That Work Well -public class OrderService : IOrderService -{ - private readonly OrderProcessingOptions _options; - private readonly IOrderRepository _repository; - - public OrderService( - IOptions options, - IOrderRepository repository) - { - _options = options.Value; - _repository = repository; - } - - public async Task ProcessAsync(Order order) - { - if (_options.EnableCaching) - { - // Check cache - } - - for (int retry = 0; retry < _options.MaxRetries; retry++) - { - try - { - return await _repository.SaveAsync(order); - } - catch (Exception) when (retry < _options.MaxRetries - 1) - { - await Task.Delay(TimeSpan.FromSeconds(1)); - } - } - - throw new Exception("Max retries exceeded"); - } -} -``` +- **Constructor injection everywhere** – middleware, handlers, lifecycle hooks can all resolve services + directly. Avoid service locator patterns unless you truly need dynamic lookups. +- **Decorator pattern** – use `builder.Services.Decorate()` (from Scrutor) to add + caching, logging, or retry behavior without touching core services. +- **Keyed services** – register multiple implementations with `AddKeyed{Lifetime}` and inject the + one you need via `[FromKeyedServices]`. -### Options Variants +### Keyed Services in Practice ```csharp title="Program.cs" -// IOptions - Singleton, value never reloads -builder.Services.AddScoped(); - -// IOptionsSnapshot - Scoped, reloads per invocation -builder.Services.AddScoped(); +builder.Services.AddKeyedSingleton("email"); +builder.Services.AddKeyedSingleton("sms"); -// IOptionsMonitor - Singleton, reloads when config changes -builder.Services.AddSingleton(); -``` - -**For Lambda, prefer `IOptions`** since configuration typically doesn't change during function execution. - -## Best Practices - -### ✅ Do: Use Scoped for Per-Invocation State - -```csharp -// GOOD: Repository with per-invocation state -builder.Services.AddScoped(); -``` - -### ❌ Don't: Use Singleton for Stateful Services - -```csharp -// BAD: Singleton with mutable state causes cross-invocation contamination -builder.Services.AddSingleton(); -``` - -### ✅ Do: Use Singleton for Shared Resources - -```csharp -// GOOD: HTTP client pool is thread-safe and benefits from reuse -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -``` - -### ✅ Do: Register Interfaces, Not Implementations - -```csharp -// GOOD: Testable and flexible -builder.Services.AddScoped(); - -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.ProcessAsync(order) -); -``` - -### ❌ Don't: Use Concrete Types Everywhere - -```csharp -// BAD: Hard to test and tightly coupled -builder.Services.AddScoped(); - -lambda.MapHandler(([Event] Order order, OrderService service) => - service.ProcessAsync(order) -); -``` - -### ✅ Do: Use Options Pattern for Configuration - -```csharp -// GOOD: Strongly-typed, testable configuration -builder.Services.Configure( - builder.Configuration.GetSection("OrderProcessing") -); -``` - -### ❌ Don't: Hardcode Configuration Values - -```csharp -// BAD: Hardcoded, difficult to test and change -public class OrderService -{ - private const int MaxRetries = 3; - private const string ApiUrl = "https://api.example.com"; -} -``` - -### ✅ Do: Dispose Resources Properly - -```csharp -// GOOD: Scoped services are automatically disposed after each invocation -builder.Services.AddScoped(); - -public class OrderRepository : IOrderRepository, IDisposable -{ - private readonly HttpClient _httpClient; - - public void Dispose() - { - _httpClient?.Dispose(); - } -} -``` - -### ✅ Do: Inject CancellationToken for Timeout Handling - -```csharp -// GOOD: Gracefully handle Lambda timeouts -lambda.MapHandler(async ( +lambda.MapHandler(( [Event] Order order, - IOrderService service, - CancellationToken cancellationToken -) => -{ - return await service.ProcessAsync(order, cancellationToken); -}); -``` - -## Anti-Patterns to Avoid - -### ❌ Service Locator Pattern - -```csharp -// BAD: Service locator anti-pattern -lambda.MapHandler(([Event] Order order, IServiceProvider services) => -{ - var service = services.GetRequiredService(); - return service.ProcessAsync(order); -}); -``` - -**Why it's bad:** -- Hides dependencies (hard to test) -- Runtime errors instead of compile-time errors -- Violates dependency inversion principle - -**Better approach:** - -```csharp -// GOOD: Explicit dependency injection -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.ProcessAsync(order) -); -``` - ---- - -### ❌ Registering Everything as Singleton - -```csharp -// BAD: All services are Singleton -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -``` - -**Why it's bad:** -- Cross-invocation state contamination -- Memory leaks (services never disposed) -- Thread-safety issues - -**Better approach:** - -```csharp -// GOOD: Use appropriate lifetimes -builder.Services.AddSingleton(); // Stateless, shared -builder.Services.AddScoped(); // Per-invocation -builder.Services.AddScoped(); // Per-invocation -``` - ---- - -### ❌ Constructor Over-Injection - -```csharp -// BAD: Too many dependencies -public class OrderService -{ - public OrderService( - IOrderRepository orderRepo, - IInventoryService inventoryService, - IPaymentService paymentService, - INotificationService notificationService, - ILogger logger, - IMetricsCollector metrics, - ICache cache, - IValidator validator, - IMapper mapper - ) - { - // ... - } -} -``` - -**Why it's bad:** -- Service Responsibility Principle violation -- Hard to test and maintain -- Usually indicates poor design - -**Better approach:** - -```csharp -// GOOD: Split into smaller services -public class OrderService -{ - public OrderService( - IOrderRepository repository, - IOrderProcessor processor, - ILogger logger - ) - { - // ... - } -} - -public class OrderProcessor -{ - public OrderProcessor( - IInventoryService inventory, - IPaymentService payment, - INotificationService notification - ) - { - // ... - } -} + [FromKeyedServices("sms")] INotifier notifier +) => notifier.NotifyAsync(order)); ``` ---- +- Keys can be strings, enums, numeric types, or even `Type` instances. +- Optional services are supported by making the parameter nullable. +- The generated code throws a descriptive exception if the service provider doesn't support keyed + services (e.g., if you run on an older DI container). -### ❌ Mixing Lifetimes Incorrectly +## Host-Specific Pitfalls -```csharp -// BAD: Singleton depends on Scoped service -builder.Services.AddSingleton(); -builder.Services.AddScoped(); - -public class OrderCache : IOrderCache -{ - public OrderCache(IOrderRepository repository) // PROBLEM! - { - // Singleton capturing Scoped dependency - } -} -``` - -**Why it's bad:** -- Scoped service is captured by Singleton -- First invocation's scoped instance is reused forever -- Cross-invocation state contamination - -**Better approach:** - -```csharp -// GOOD: Inject IServiceProvider and resolve per-invocation -public class OrderCache : IOrderCache -{ - private readonly IServiceProvider _serviceProvider; - - public OrderCache(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public async Task GetAsync(string id) - { - using var scope = _serviceProvider.CreateScope(); - var repository = scope.ServiceProvider.GetRequiredService(); - return await repository.GetAsync(id); - } -} -``` - -## Common Patterns - -### Repository Pattern - -```csharp title="Program.cs" -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -var lambda = builder.Build(); - -lambda.MapHandler(([Event] OrderRequest request, IOrderService service) => - service.ProcessAsync(request) -); -``` - -```csharp title="Services/OrderService.cs" -public class OrderService : IOrderService -{ - private readonly IOrderRepository _repository; - - public OrderService(IOrderRepository repository) - { - _repository = repository; - } - - public async Task ProcessAsync(OrderRequest request) - { - var order = new Order(request.Id, request.Amount); - await _repository.SaveAsync(order); - return new OrderResponse(order.Id, true); - } -} -``` - -### Unit of Work Pattern - -```csharp title="Program.cs" -builder.Services.AddScoped(); -builder.Services.AddScoped(sp => - sp.GetRequiredService().Orders -); - -var lambda = builder.Build(); -``` - -```csharp title="Data/UnitOfWork.cs" -public class UnitOfWork : IUnitOfWork, IDisposable -{ - private readonly DbContext _context; - - public UnitOfWork(DbContext context) - { - _context = context; - Orders = new OrderRepository(context); - } - - public IOrderRepository Orders { get; } - - public async Task CommitAsync() => await _context.SaveChangesAsync(); - - public void Dispose() => _context?.Dispose(); -} -``` - -### Decorator Pattern - -```csharp title="Program.cs" -builder.Services.AddScoped(); -builder.Services.Decorate(); - -var lambda = builder.Build(); -``` - -```csharp title="Services/CachedOrderService.cs" -public class CachedOrderService : IOrderService -{ - private readonly IOrderService _inner; - private readonly ICache _cache; - - public CachedOrderService(IOrderService inner, ICache cache) - { - _inner = inner; - _cache = cache; - } - - public async Task ProcessAsync(OrderRequest request) - { - var cacheKey = $"order:{request.Id}"; - if (_cache.TryGet(cacheKey, out OrderResponse cached)) - { - return cached; - } - - var result = await _inner.ProcessAsync(request); - _cache.Set(cacheKey, result); - return result; - } -} -``` - -## Troubleshooting - -### Service Not Found - -**Error:** - -``` -InvalidOperationException: No service for type 'IOrderService' has been registered. -``` - -**Solution:** - -Ensure the service is registered before building: - -```csharp -builder.Services.AddScoped(); -var lambda = builder.Build(); -``` - -### Singleton Capturing Scoped Dependency - -**Problem:** Singleton service depends on Scoped service. - -**Solution:** Use `IServiceProvider` to resolve Scoped services dynamically: - -```csharp -public class MySingletonService -{ - private readonly IServiceProvider _serviceProvider; - - public MySingletonService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public async Task DoWorkAsync() - { - using var scope = _serviceProvider.CreateScope(); - var scopedService = scope.ServiceProvider.GetRequiredService(); - await scopedService.DoWorkAsync(); - } -} -``` - -### Disposed Service Access - -**Error:** - -``` -ObjectDisposedException: Cannot access a disposed object. -``` - -**Problem:** Attempting to use a Scoped service after the invocation completes. - -**Solution:** Ensure services are only used within the invocation scope. Don't store Scoped services in Singleton fields. +| Pitfall | Impact | Fix | +|---------|--------|-----| +| Singleton depends on a scoped service | Scoped instance from first invocation leaks forever | Inject `IServiceProvider`, create a scope, resolve the scoped service inside the method | +| Storing scoped services in singletons | `ObjectDisposedException` on later invocations | Keep scoped dependencies scoped; pass data instead of services | +| Over-injecting handlers | Hard-to-test functions with 8+ services | Move orchestration into services; keep handlers thin | +| Forgetting cancellation tokens | Lambda kills the environment mid-work | Always inject `CancellationToken` and pass it down | ## Key Takeaways -1. **Understand Lifetimes**: Use Singleton for stateless shared resources, Scoped for per-invocation services -2. **Inject Dependencies**: Use constructor injection, not service locator pattern -3. **Options Pattern**: Use `IOptions` for strongly-typed configuration -4. **CancellationToken**: Always inject `CancellationToken` for timeout handling -5. **Avoid Anti-Patterns**: Don't mix lifetimes incorrectly or over-inject dependencies -6. **Reusable Middleware**: Create middleware classes with constructor injection -7. **Lifecycle DI**: Use `IServiceProvider` in `OnInit` and `OnShutdown` handlers - -## Next Steps - -Now that you understand dependency injection, explore related topics: - -- **[Middleware](/guides/middleware.md)** – Build middleware pipelines with DI -- **[Lifecycle Management](/guides/lifecycle-management.md)** – Use DI in OnInit and OnShutdown -- **[Configuration](/guides/configuration.md)** – Advanced configuration patterns -- **[Testing](/guides/testing.md)** – Test services with dependency injection -- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns with DI - ---- +- Register everything before `builder.Build()` so the container is ready for cold starts. +- Map lifetimes to the Lambda lifecycle: singleton for shared resources, scoped for per-invocation work, transient only when necessary. +- Always pass the host-provided `CancellationToken`; adjust `InvocationCancellationBuffer` when you need more time to wind down. +- Prefer constructor injection in handlers/middleware/lifecycle hooks—avoid service locator patterns. +- Use the options pattern for config and keyed services for multiple implementations. +- For fundamentals, refer back to the [official DI docs](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection). -Congratulations! You now understand how to leverage dependency injection to build maintainable, testable Lambda functions. +With these patterns, aws-lambda-host feels just like ASP.NET Core, but tuned for the Lambda lifecycle. From fe15ef25ddf8d816afe534ea87a2c9fb7cb4f079 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 12:28:53 -0500 Subject: [PATCH 44/65] feat(docs): simplify and streamline middleware guide - Removed redundant and overly detailed examples for concise documentation. - Replaced repetitive setup instructions with clear pipeline execution basics. - Enhanced clarity of `ILambdaHostContext` usage and feature access explanations. - Introduced simplified tables and sections for improved readability and structure. - Updated middleware examples to use consistent logging with reduced verbosity. --- docs/guides/middleware.md | 912 +++++--------------------------------- 1 file changed, 113 insertions(+), 799 deletions(-) diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index 46b2793f..bfc378fc 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -1,215 +1,92 @@ # Middleware -Middleware provides a powerful mechanism for building composable pipelines around your Lambda handlers. Inspired by ASP.NET Core middleware, aws-lambda-host middleware enables cross-cutting concerns like logging, validation, metrics, error handling, and more—all without cluttering your handler code. +`aws-lambda-host` uses the same middleware model as ASP.NET Core: each component gets a context object, +runs code before/after the next component, and can short-circuit the pipeline. If you're new to the +pattern, skim the [ASP.NET Core middleware overview](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/) +first. This guide focuses on Lambda-specific behavior: invocation scopes, feature access, and +composition tips. -## Introduction +## Pipeline Basics -Middleware components execute in sequence, forming a pipeline around your handler: - -``` -Request → Middleware 1 → Middleware 2 → Handler → Middleware 2 → Middleware 1 → Response -``` - -Each middleware can: - -- Inspect the request before the handler executes -- Short-circuit the pipeline (skip the handler) -- Execute logic after the handler completes -- Handle exceptions from downstream components - -## Basic Middleware - -### Inline Middleware - -The simplest form of middleware is an inline lambda function: +Register middleware before calling `MapHandler`. Components execute in registration order and unwind in +reverse order: ```csharp title="Program.cs" -using AwsLambda.Host; - var builder = LambdaApplication.CreateBuilder(); var lambda = builder.Build(); lambda.UseMiddleware(async (context, next) => { - Console.WriteLine("Before handler"); - await next(context); - Console.WriteLine("After handler"); -}); - -lambda.MapHandler(([Event] Request request) => -{ - Console.WriteLine("Handler executing"); - return new Response("Success"); -}); - -await lambda.RunAsync(); -``` - -**Output:** -``` -Before handler -Handler executing -After handler -``` - -### Middleware Pipeline Composition - -Multiple middleware components execute in the order they're registered: - -```csharp title="Program.cs" -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine("[Middleware 1]: Before"); + Console.WriteLine("[Logging] Before handler"); await next(context); - Console.WriteLine("[Middleware 1]: After"); + Console.WriteLine("[Logging] After handler"); }); lambda.UseMiddleware(async (context, next) => { - Console.WriteLine("[Middleware 2]: Before"); + Console.WriteLine("[Metrics] Before handler"); await next(context); - Console.WriteLine("[Middleware 2]: After"); + Console.WriteLine("[Metrics] After handler"); }); -lambda.MapHandler(([Event] Request request) => -{ - Console.WriteLine("[Handler]: Executing"); - return new Response("Success"); -}); +lambda.MapHandler(([Event] Request request) => new Response("ok")); +await lambda.RunAsync(); ``` -**Output:** +Output: + ``` -[Middleware 1]: Before -[Middleware 2]: Before -[Handler]: Executing -[Middleware 2]: After -[Middleware 1]: After +[Logging] Before handler +[Metrics] Before handler +[Metrics] After handler +[Logging] After handler ``` -## ILambdaHostContext - -Middleware receives an `ILambdaHostContext` which provides access to invocation details and services. +## `ILambdaHostContext` -### Context Properties +Every middleware receives the same `ILambdaHostContext`, which is scoped to the invocation. ```csharp title="Program.cs" lambda.UseMiddleware(async (context, next) => { - // Access DI container var logger = context.ServiceProvider.GetRequiredService>(); - // Access cancellation token if (context.CancellationToken.IsCancellationRequested) { - logger.LogWarning("Request cancelled before handler"); + logger.LogWarning("Invocation cancelled before handler"); return; } - // Store invocation-scoped data context.Items["RequestId"] = Guid.NewGuid().ToString(); - context.Items["StartTime"] = DateTimeOffset.UtcNow; + context.Properties["Version"] ??= "1.0.0"; // safe cross-invocation value await next(context); - // Access stored data - var startTime = (DateTimeOffset)context.Items["StartTime"]; - var duration = DateTimeOffset.UtcNow - startTime; - logger.LogInformation("Request completed in {DurationMs}ms", duration.TotalMilliseconds); + var started = (DateTimeOffset)context.Items["Start"]; + logger.LogInformation("Completed in {Duration}ms", (DateTimeOffset.UtcNow - started).TotalMilliseconds); }); ``` -### Accessing Event and Response - -Use the `Features` collection to access typed event and response data: - -```csharp title="Program.cs" -using AwsLambda.Host.Abstractions.Features; +Key members: -lambda.UseMiddleware(async (context, next) => -{ - // Access the deserialized event - var eventFeature = context.Features.Get>(); - if (eventFeature != null) - { - var request = eventFeature.Event; - Console.WriteLine($"Processing request: {request.Name}"); - } +- `ServiceProvider` – resolve scoped services for the invocation. +- `CancellationToken` – fires before Lambda termination (buffer controlled by + `LambdaHostOptions.InvocationCancellationBuffer`). Pass it to downstream async work. +- `Items` – per-invocation storage shared by middleware/handler. +- `Properties` – cross-invocation storage. +- `Features` – type-keyed feature objects such as `IEventFeature` + and `IResponseFeature`. - await next(context); +## Inline Middleware Only - // Access the response - var responseFeature = context.Features.Get>(); - if (responseFeature != null) - { - var response = responseFeature.Response; - Console.WriteLine($"Response: {response.Message}"); - } -}); -``` +`UseMiddleware` currently accepts inline delegates. Class-based middleware activators are on the roadmap, +so for now keep middleware logic inside the lambda or extract helper services for reuse. -## Common Middleware Patterns +## Working with Features -### Logging Middleware - -```csharp title="Program.cs" -using System.Diagnostics; - -lambda.UseMiddleware(async (context, next) => -{ - var logger = context.ServiceProvider.GetRequiredService>(); - var stopwatch = Stopwatch.StartNew(); - - logger.LogInformation("Request starting"); - - try - { - await next(context); - stopwatch.Stop(); - logger.LogInformation("Request completed successfully in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); - } - catch (Exception ex) - { - stopwatch.Stop(); - logger.LogError(ex, "Request failed after {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); - throw; - } -}); -``` - -### Error Handling Middleware - -```csharp title="Program.cs" -lambda.UseMiddleware(async (context, next) => -{ - try - { - await next(context); - } - catch (ValidationException ex) - { - var logger = context.ServiceProvider.GetRequiredService>(); - logger.LogWarning(ex, "Validation failed: {Message}", ex.Message); - - // Set error response - var responseFeature = context.Features.Get>(); - if (responseFeature != null) - { - responseFeature.Response = new Response($"Validation error: {ex.Message}"); - } - - // Don't re-throw - error handled gracefully - } - catch (Exception ex) - { - var logger = context.ServiceProvider.GetRequiredService>(); - logger.LogError(ex, "Unhandled exception"); - throw; // Re-throw unhandled exceptions - } -}); -``` - -### Validation Middleware +Features are type-keyed adapters stored inside `ILambdaHostContext.Features` (an +`IFeatureCollection`). They decouple middleware from handlers: a handler (or the framework) populates a +feature, middleware reads or mutates it, and nobody needs to inject each other through DI. ```csharp title="Program.cs" using AwsLambda.Host.Abstractions.Features; @@ -217,698 +94,135 @@ using AwsLambda.Host.Abstractions.Features; lambda.UseMiddleware(async (context, next) => { var eventFeature = context.Features.Get>(); - if (eventFeature != null) - { - var request = eventFeature.Event; - - // Validate request - if (string.IsNullOrEmpty(request.OrderId)) - { - throw new ValidationException("OrderId is required"); - } - - if (request.Amount <= 0) - { - throw new ValidationException("Amount must be positive"); - } - } + if (eventFeature is { Event: { } request }) + Console.WriteLine($"Processing {request.OrderId}"); await next(context); -}); -``` - -### Metrics Middleware - -```csharp title="Program.cs" -using System.Diagnostics; - -lambda.UseMiddleware(async (context, next) => -{ - var metrics = context.ServiceProvider.GetRequiredService(); - var stopwatch = Stopwatch.StartNew(); - - metrics.IncrementCounter("requests.total"); - - try - { - await next(context); - - stopwatch.Stop(); - metrics.RecordDuration("requests.duration", stopwatch.Elapsed); - metrics.IncrementCounter("requests.success"); - } - catch (Exception) - { - stopwatch.Stop(); - metrics.IncrementCounter("requests.failed"); - throw; - } -}); -``` - -### Caching Middleware - -```csharp title="Program.cs" -using AwsLambda.Host.Abstractions.Features; - -lambda.UseMiddleware(async (context, next) => -{ - var cache = context.ServiceProvider.GetRequiredService(); - var eventFeature = context.Features.Get>(); - - if (eventFeature != null) - { - var request = eventFeature.Event; - var cacheKey = $"request:{request.Id}"; - - // Check cache - if (cache.TryGet(cacheKey, out var cachedResponse)) - { - var responseFeature = context.Features.Get>(); - if (responseFeature != null) - { - responseFeature.Response = cachedResponse; - } - return; // Short-circuit - skip handler - } - - // Execute handler - await next(context); - // Cache response - var finalResponseFeature = context.Features.Get>(); - if (finalResponseFeature?.Response != null) - { - cache.Set(cacheKey, finalResponseFeature.Response, TimeSpan.FromMinutes(5)); - } - } - else - { - await next(context); - } + var responseFeature = context.Features.Get>(); + if (responseFeature?.Response is { } response) + Console.WriteLine($"Result: {response.Status}"); }); ``` -## Class-Based Middleware +Common features: -For reusable middleware, create a class with constructor injection. +| Feature | Purpose | +|-------------------------------|--------------------------------------------------------------| +| `IEventFeature` | Access the deserialized event payload | +| `IResponseFeature` | Inspect or replace the handler response before serialization | +| `IInvocationDataFeature` | Access raw event/response streams for envelopes | -### Defining Middleware Classes +**Why features matter:** -```csharp title="Middleware/LoggingMiddleware.cs" -using AwsLambda.Host.Abstractions; -using Microsoft.Extensions.Logging; +- Middleware can extract values set by handlers (or other middleware) without DI fan-out. +- Handlers remain free of middleware-specific dependencies; they just work with the event/response types. +- Custom features are easy to add—define an interface, set it on `context.Features`, and retrieve it + downstream when needed. -public class LoggingMiddleware +```csharp title="Custom feature" +public interface ICorrelationFeature { - private readonly ILogger _logger; - - public LoggingMiddleware(ILogger logger) - { - _logger = logger; - } - - public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) - { - _logger.LogInformation("Request starting"); - - try - { - await next(context); - _logger.LogInformation("Request completed successfully"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Request failed"); - throw; - } - } + string CorrelationId { get; set; } } -``` - -### Registering Class-Based Middleware - -```csharp title="Program.cs" -using AwsLambda.Host; -using Microsoft.Extensions.DependencyInjection; - -var builder = LambdaApplication.CreateBuilder(); - -// Register middleware as Singleton -builder.Services.AddSingleton(); - -var lambda = builder.Build(); - -// Use class-based middleware -lambda.UseMiddleware(); -lambda.MapHandler(([Event] Request request) => new Response("Success")); - -await lambda.RunAsync(); -``` - -### Multiple Class-Based Middleware - -```csharp title="Program.cs" -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -var lambda = builder.Build(); - -// Execution order: Logging → Metrics → Validation → Handler -lambda.UseMiddleware(); -lambda.UseMiddleware(); -lambda.UseMiddleware(); - -lambda.MapHandler(([Event] Request request) => new Response("Success")); -``` - -## Advanced Patterns - -### Conditional Middleware - -Execute middleware only under certain conditions: - -```csharp title="Program.cs" -lambda.UseMiddleware(async (context, next) => +public sealed class CorrelationFeature : ICorrelationFeature { - var eventFeature = context.Features.Get>(); - - // Only log for specific request types - if (eventFeature?.Event?.Type == "order") - { - var logger = context.ServiceProvider.GetRequiredService>(); - logger.LogInformation("Processing order request"); - } - - await next(context); -}); -``` - -### Short-Circuiting - -Skip the handler and downstream middleware: - -```csharp title="Program.cs" -lambda.UseMiddleware(async (context, next) => -{ - var cache = context.ServiceProvider.GetRequiredService(); - var eventFeature = context.Features.Get>(); - - if (eventFeature != null && cache.TryGet(eventFeature.Event.Id, out Response cached)) - { - var responseFeature = context.Features.Get>(); - if (responseFeature != null) - { - responseFeature.Response = cached; - } - return; // SHORT-CIRCUIT: Skip handler - } - - await next(context); // Continue pipeline -}); -``` - -### Middleware with Configuration - -```csharp title="Middleware/CachingMiddleware.cs" -using AwsLambda.Host.Abstractions; -using Microsoft.Extensions.Options; - -public class CachingMiddleware -{ - private readonly ICache _cache; - private readonly CachingOptions _options; - - public CachingMiddleware(ICache cache, IOptions options) - { - _cache = cache; - _options = options.Value; - } - - public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) - { - if (!_options.Enabled) - { - await next(context); - return; - } - - var eventFeature = context.Features.Get>(); - if (eventFeature != null) - { - var cacheKey = $"request:{eventFeature.Event.Id}"; - - if (_cache.TryGet(cacheKey, out var cached)) - { - var responseFeature = context.Features.Get>(); - if (responseFeature != null) - { - responseFeature.Response = cached; - } - return; - } - - await next(context); - - var finalResponseFeature = context.Features.Get>(); - if (finalResponseFeature?.Response != null) - { - _cache.Set(cacheKey, finalResponseFeature.Response, _options.CacheDuration); - } - } - else - { - await next(context); - } - } + public string CorrelationId { get; set; } = Guid.NewGuid().ToString(); } -``` - -```csharp title="Program.cs" -builder.Services.Configure( - builder.Configuration.GetSection("Caching") -); - -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -var lambda = builder.Build(); -lambda.UseMiddleware(); -``` -### Middleware Factory Pattern - -```csharp title="Program.cs" lambda.UseMiddleware(async (context, next) => { - var factory = context.ServiceProvider.GetRequiredService(); - var customMiddleware = factory.Create("validation"); - - await customMiddleware.InvokeAsync(context, next); -}); -``` - -## Execution Order - -Understanding middleware execution order is critical for building correct pipelines. - -### Registration Order - -Middleware executes in the order registered: - -```csharp title="Program.cs" -// Execution order: 1 → 2 → 3 → Handler → 3 → 2 → 1 -lambda.UseMiddleware(); // 1 -lambda.UseMiddleware(); // 2 -lambda.UseMiddleware(); // 3 -lambda.MapHandler(/* handler */); -``` - -### Typical Ordering - -```csharp title="Program.cs" -// 1. Error handling (catch all exceptions) -lambda.UseMiddleware(); - -// 2. Logging (log all requests) -lambda.UseMiddleware(); - -// 3. Metrics (measure all requests) -lambda.UseMiddleware(); - -// 4. Authentication (verify identity) -lambda.UseMiddleware(); - -// 5. Authorization (check permissions) -lambda.UseMiddleware(); - -// 6. Validation (validate input) -lambda.UseMiddleware(); - -// 7. Caching (cache responses) -lambda.UseMiddleware(); - -// 8. Handler -lambda.MapHandler(/* handler */); -``` - -**Why this order?** - -- **Error handling first** – Catches exceptions from all downstream middleware -- **Logging early** – Logs all requests (including failures) -- **Metrics early** – Measures all requests (including failures) -- **Auth before validation** – No point validating unauthenticated requests -- **Caching last** – Only cache authenticated, authorized, validated requests - -## State Management - -### Context.Items (Per-Invocation) - -Store temporary data scoped to a single invocation: - -```csharp title="Program.cs" -lambda.UseMiddleware(async (context, next) => -{ - // Set invocation-scoped data - context.Items["RequestId"] = Guid.NewGuid().ToString(); - context.Items["UserId"] = "user123"; + var feature = context.Features.Get() ?? new CorrelationFeature(); + context.Features.Set(feature); // make it visible to downstream components + context.Items["CorrelationId"] = feature.CorrelationId; await next(context); }); lambda.UseMiddleware(async (context, next) => { - // Access invocation-scoped data from previous middleware - var requestId = context.Items["RequestId"] as string; - var userId = context.Items["UserId"] as string; - - Console.WriteLine($"Request {requestId} for user {userId}"); - + var correlation = context.Features.Get()?.CorrelationId; + Console.WriteLine($"Tracking {correlation}"); await next(context); }); ``` -**Cleared after each invocation.** - -### Context.Properties (Cross-Invocation) +## Short-Circuiting and Error Handling -Store shared data configured during the build phase: - -```csharp title="Program.cs" -var lambda = builder.Build(); - -// Set cross-invocation properties -lambda.Properties["Version"] = "1.0.0"; -lambda.Properties["Environment"] = "Production"; +Middleware can stop the pipeline early: +```csharp title="Caching" lambda.UseMiddleware(async (context, next) => { - // Access shared properties - var version = context.Properties["Version"] as string; - var env = context.Properties["Environment"] as string; - - Console.WriteLine($"App version {version} running in {env}"); - - await next(context); -}); -``` - -**Persists across invocations.** - -## Best Practices - -### ✅ Do: Keep Middleware Focused - -```csharp -// GOOD: Single responsibility -public class LoggingMiddleware -{ - public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) - { - _logger.LogInformation("Request starting"); - await next(context); - _logger.LogInformation("Request completed"); - } -} -``` - -### ❌ Don't: Mix Concerns in Middleware - -```csharp -// BAD: Too many responsibilities -public class EverythingMiddleware -{ - public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) - { - // Logging - _logger.LogInformation("Request starting"); - - // Metrics - _metrics.IncrementCounter("requests"); - - // Validation - if (string.IsNullOrEmpty(request.Id)) throw new ValidationException(); - - // Caching - if (_cache.TryGet(key, out var cached)) return cached; - - await next(context); - - // More logic... - } -} -``` - -### ✅ Do: Use Class-Based Middleware for Reusability - -```csharp -// GOOD: Reusable across projects -public class MetricsMiddleware -{ - private readonly IMetricsCollector _metrics; - - public MetricsMiddleware(IMetricsCollector metrics) - { - _metrics = metrics; - } + var cache = context.ServiceProvider.GetRequiredService(); + var request = context.Features.Get>()?.Event; - public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next) + if (request is not null && cache.TryGet(request.OrderId, out OrderResponse cached)) { - _metrics.IncrementCounter("requests.total"); - await next(context); + context.Features.Get>()!.Response = cached; + return; // skip handler } -} -``` - -### ✅ Do: Handle Exceptions Appropriately -```csharp -// GOOD: Catch, log, and re-throw -lambda.UseMiddleware(async (context, next) => -{ - try - { - await next(context); - } - catch (Exception ex) - { - _logger.LogError(ex, "Request failed"); - throw; // Re-throw to let Lambda handle it - } + await next(context); }); ``` -### ❌ Don't: Swallow Exceptions Silently +Wrap the pipeline to catch and translate exceptions: -```csharp -// BAD: Silent failure +```csharp title="Error Handling" lambda.UseMiddleware(async (context, next) => { try { await next(context); } - catch + catch (ValidationException ex) { - // Exception swallowed - Lambda thinks it succeeded! + var response = context.Features.Get>(); + if (response is not null) + response.Response = new("invalid", ex.Message); + return; // handled } }); ``` -### ✅ Do: Order Middleware Intentionally - -```csharp -// GOOD: Error handling wraps everything -lambda.UseMiddleware(); -lambda.UseMiddleware(); -lambda.UseMiddleware(); -``` - -### ❌ Don't: Place Error Handling Last - -```csharp -// BAD: Error handling can't catch validation errors -lambda.UseMiddleware(); -lambda.UseMiddleware(); // Too late! -``` - -## Anti-Patterns to Avoid - -### ❌ Blocking Async Code - -```csharp -// BAD: Blocking async operations -lambda.UseMiddleware(async (context, next) => -{ - var result = SomeAsyncMethod().Result; // DON'T! - await next(context); -}); -``` - -**Better approach:** - -```csharp -// GOOD: Proper async/await -lambda.UseMiddleware(async (context, next) => -{ - var result = await SomeAsyncMethod(); - await next(context); -}); -``` - ---- - -### ❌ Forgetting to Call next() - -```csharp -// BAD: Handler never executes -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine("Before handler"); - // Forgot to call next(context) - handler never runs! -}); -``` - -**Better approach:** - -```csharp -// GOOD: Always call next() unless short-circuiting intentionally -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine("Before handler"); - await next(context); - Console.WriteLine("After handler"); -}); -``` - ---- - -### ❌ Mutating Context Incorrectly - -```csharp -// BAD: Replacing context breaks downstream middleware -lambda.UseMiddleware(async (context, next) => -{ - context = new LambdaHostContext(); // DON'T! - await next(context); -}); -``` - -**Better approach:** - -```csharp -// GOOD: Use Items or Properties to store data -lambda.UseMiddleware(async (context, next) => -{ - context.Items["CustomData"] = "value"; - await next(context); -}); -``` +## Ordering Strategy -## Testing Middleware +Register middleware from outermost to innermost: -### Unit Testing Inline Middleware - -```csharp title="Tests/MiddlewareTests.cs" -using Xunit; -using NSubstitute; -using AwsLambda.Host.Abstractions; - -public class MiddlewareTests -{ - [Fact] - public async Task LoggingMiddleware_LogsBeforeAndAfter() - { - // Arrange - var context = Substitute.For(); - var logger = Substitute.For>(); - var serviceProvider = Substitute.For(); - - serviceProvider.GetService(typeof(ILogger)).Returns(logger); - context.ServiceProvider.Returns(serviceProvider); - - var nextCalled = false; - Task Next(ILambdaHostContext ctx) - { - nextCalled = true; - return Task.CompletedTask; - } - - // Act - await LoggingMiddleware(context, Next); - - // Assert - Assert.True(nextCalled); - logger.Received(1).LogInformation(Arg.Any()); - } - - private async Task LoggingMiddleware(ILambdaHostContext context, LambdaInvocationDelegate next) - { - var logger = context.ServiceProvider.GetRequiredService>(); - logger.LogInformation("Request starting"); - await next(context); - } -} -``` - -### Unit Testing Class-Based Middleware - -```csharp title="Tests/LoggingMiddlewareTests.cs" -using Xunit; -using NSubstitute; -using AwsLambda.Host.Abstractions; - -public class LoggingMiddlewareTests -{ - [Fact] - public async Task InvokeAsync_LogsRequestStartAndCompletion() - { - // Arrange - var logger = Substitute.For>(); - var middleware = new LoggingMiddleware(logger); - var context = Substitute.For(); - - var nextCalled = false; - Task Next(ILambdaHostContext ctx) - { - nextCalled = true; - return Task.CompletedTask; - } - - // Act - await middleware.InvokeAsync(context, Next); - - // Assert - Assert.True(nextCalled); - logger.Received(1).LogInformation("Request starting"); - logger.Received(1).LogInformation("Request completed successfully"); - } -} +```csharp title="Order" +lambda.UseMiddleware(); // catches everything +lambda.UseMiddleware(); // logs every request +lambda.UseMiddleware(); // records durations +lambda.UseMiddleware(); // auth first +lambda.UseMiddleware(); // then authorization +lambda.UseMiddleware(); // validate payloads +lambda.MapHandler(/* handler */); ``` -## Key Takeaways +Guidelines: -1. **Middleware Pipelines** – Compose cross-cutting concerns around handlers -2. **Execution Order** – Middleware executes in registration order -3. **ILambdaHostContext** – Access services, items, and features -4. **Class-Based Middleware** – Reusable with constructor injection -5. **Short-Circuiting** – Skip handler by not calling `next()` -6. **Error Handling** – Place error-handling middleware first -7. **State Management** – Use `Items` (per-invocation) or `Properties` (shared) -8. **Testing** – Unit test middleware in isolation +- Error/diagnostics (logging, metrics) go first so they see every request. +- Authentication/authorization should wrap validation and business logic. +- Response caching happens late so only valid, authorized responses are stored. -## Next Steps +## Configuration and Options -Now that you understand middleware, explore related topics: +Even though middleware delegates are inline, they still run inside the invocation scope. Resolve options +or services via `context.ServiceProvider` the same way you would inside a handler. -- **[Dependency Injection](/guides/dependency-injection.md)** – Inject services into middleware -- **[Lifecycle Management](/guides/lifecycle-management.md)** – OnInit and OnShutdown hooks -- **[Error Handling](/guides/error-handling.md)** – Build error-handling middleware -- **[Testing](/guides/testing.md)** – Test middleware components -- **[Handler Registration](/guides/handler-registration.md)** – Understand handler execution - ---- +## Best Practices -Congratulations! You now understand how to build composable middleware pipelines for your Lambda functions. +- **Keep middleware focused.** One responsibility per component (logging, metrics, caching, etc.). +- **Always call `await next(context)`** unless you intentionally short-circuit; forgetting it prevents the + handler from running. +- **Never swallow exceptions silently.** If you handle an error, set a response or log it so Lambda doesn’t + report success unintentionally. +- **Use per-invocation state wisely.** `Items` is cleared after each request; `Properties` live for the life + of the container and must be thread-safe. +- **Prefer class-based middleware** for anything reusable or needing DI/Options. +- **Make cancellation cooperative.** Honor `context.CancellationToken` in middleware and pass it to downstream I/O. + +With these patterns, you can build rich, testable pipelines around your Lambda handlers while keeping +business logic small and focused. From 39b99b18e403f9a52a4913174be93c13de402e20 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 12:36:16 -0500 Subject: [PATCH 45/65] feat(docs): enhance middleware guide with `IFeatureProvider` details and improved examples - Added explanation of lazy feature creation using `IFeatureProvider` in `ILambdaHostContext.Features`. - Updated middleware example to demonstrate `CorrelationFeatureProvider` usage for added clarity. - Simplified steps for adding custom features and registering providers to reduce verbosity. - Improved formatting and example code consistency for better readability. --- docs/guides/middleware.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index bfc378fc..c72c02e8 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -86,7 +86,9 @@ so for now keep middleware logic inside the lambda or extract helper services fo Features are type-keyed adapters stored inside `ILambdaHostContext.Features` (an `IFeatureCollection`). They decouple middleware from handlers: a handler (or the framework) populates a -feature, middleware reads or mutates it, and nobody needs to inject each other through DI. +feature, middleware reads or mutates it, and nobody needs to inject each other through DI. The +collection lazily creates features by asking every registered `IFeatureProvider` to build them when +first requested. ```csharp title="Program.cs" using AwsLambda.Host.Abstractions.Features; @@ -117,8 +119,7 @@ Common features: - Middleware can extract values set by handlers (or other middleware) without DI fan-out. - Handlers remain free of middleware-specific dependencies; they just work with the event/response types. -- Custom features are easy to add—define an interface, set it on `context.Features`, and retrieve it - downstream when needed. +- Custom features are easy to add—Just add an implementation of `IFeatureProvider` and register it. It will then be available to all middleware. ```csharp title="Custom feature" public interface ICorrelationFeature @@ -134,18 +135,23 @@ public sealed class CorrelationFeature : ICorrelationFeature lambda.UseMiddleware(async (context, next) => { var feature = context.Features.Get() ?? new CorrelationFeature(); - context.Features.Set(feature); // make it visible to downstream components - + context.Items["CorrelationId"] = feature.CorrelationId; await next(context); }); -lambda.UseMiddleware(async (context, next) => +public sealed class CorrelationFeatureProvider : IFeatureProvider { - var correlation = context.Features.Get()?.CorrelationId; - Console.WriteLine($"Tracking {correlation}"); - await next(context); -}); + private static readonly Type FeatureType = typeof(ICorrelationFeature); + + public bool TryCreate(Type type, out object? feature) + { + feature = type == FeatureType ? new CorrelationFeature() : null; + return feature is not null; + } +} + +builder.Services.AddSingleton(); ``` ## Short-Circuiting and Error Handling From a5f04d4eecd114cbb2e9e30917756f846d9e7616 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 12:48:35 -0500 Subject: [PATCH 46/65] feat(docs): streamline lifecycle management guide - Condensed sections on `OnInit`, `Invocation`, and `OnShutdown` phases for better readability. - Removed redundant examples and replaced them with concise, practical usage scenarios. - Enhanced lifecycle handler documentation with clear guidelines on best practices and DI usage. - Added structured logging examples and clarified `ClearLambdaOutputFormatting` benefits. - Improved formatting and example consistency across lifecycle phases for easier understanding. --- docs/guides/lifecycle-management.md | 840 ++++------------------------ 1 file changed, 101 insertions(+), 739 deletions(-) diff --git a/docs/guides/lifecycle-management.md b/docs/guides/lifecycle-management.md index b3d223bd..b716b8b8 100644 --- a/docs/guides/lifecycle-management.md +++ b/docs/guides/lifecycle-management.md @@ -1,861 +1,223 @@ # Lifecycle Management -AWS Lambda functions have three distinct execution phases: **Init**, **Invocation**, and **Shutdown**. Understanding and leveraging these phases is key to building high-performance, resource-efficient Lambda functions. +`AwsLambda.Host` exposes the entire Lambda container lifecycle so you can prepare resources during cold start, react to each invocation, and cleanly shut down when AWS reclaims the execution environment. -## Introduction - -Lambda execution follows this lifecycle: +## Execution Flow at a Glance ``` -Cold Start → OnInit → Invocation 1 → Invocation 2 → ... → OnShutdown → Termination +Cold Start → OnInit → Invocation 1..N → OnShutdown → Termination ``` -- **OnInit**: Runs once during cold start (function initialization) -- **Invocation**: Runs for each Lambda event -- **OnShutdown**: Runs once before Lambda container terminates - -aws-lambda-host provides explicit control over each phase through lifecycle handlers. - -## Lambda Lifecycle Phases - -### Phase 1: OnInit (Cold Start) - -The OnInit phase executes once when Lambda initializes a new execution environment. Use this phase for: - -- Warming up caches -- Establishing database connections -- Preloading configuration -- Initializing HTTP clients -- Loading machine learning models - -**Characteristics:** -- Runs **once per execution environment** -- Executes **before the first invocation** -- Shares execution time with the first invocation -- Multiple handlers execute **concurrently** - -### Phase 2: Invocation - -The invocation phase processes each incoming Lambda event. This is where your handler logic executes. - -**Characteristics:** -- Runs **for each event** -- Isolated scope per invocation -- Scoped services created fresh for each invocation - -### Phase 3: OnShutdown - -The OnShutdown phase executes once before Lambda terminates the execution environment. Use this phase for: +- **OnInit** – Runs once per execution environment before the first invocation. Used to warm caches, hydrate clients, or clear Lambda-specific defaults. +- **Invocation** – Runs for every event in the normal middleware/handler pipeline. Cancellation tokens respect `InvocationCancellationBuffer`. +- **OnShutdown** – Runs once when the runtime receives SIGTERM or when the host stops. Used to flush telemetry or close connections. -- Flushing logs and metrics -- Closing database connections -- Cleaning up temporary resources -- Graceful shutdown of background tasks +Both OnInit and OnShutdown execute outside the invocation pipeline, but aws-lambda-host creates a brand-new `IServiceScope` for every handler so you can resolve scoped services safely. -**Characteristics:** -- Runs **once before termination** -- Triggered by SIGTERM signal -- Limited time to complete (configurable) -- Multiple handlers execute **sequentially** - -## OnInit Handlers - -### Basic OnInit +## OnInit: Cold Start Hooks ```csharp title="Program.cs" using AwsLambda.Host; using Microsoft.Extensions.DependencyInjection; var builder = LambdaApplication.CreateBuilder(); -var lambda = builder.Build(); -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +builder.Services.ConfigureLambdaHostOptions(options => { - Console.WriteLine("Initializing Lambda function..."); - - // Perform one-time setup - var cache = services.GetRequiredService(); - await cache.WarmUpAsync(ct); - - return true; // true = continue, false = abort initialization + options.ClearLambdaOutputFormatting = true; // Stop Lambda from wrapping console output + options.InitTimeout = TimeSpan.FromSeconds(10); // Optional override }); -lambda.MapHandler(([Event] Request request) => new Response("Success")); - -await lambda.RunAsync(); -``` - -**Return Value:** -- `true` – Continue initialization -- `false` – Abort initialization (Lambda reports failure) - -### Multiple OnInit Handlers - -Register multiple initialization handlers—they execute **concurrently**: +var lambda = builder.Build(); -```csharp title="Program.cs" -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +lambda.OnInit(async (IDistributedCache cache, ILogger logger, CancellationToken ct) => { - Console.WriteLine("Warming up cache"); - var cache = services.GetRequiredService(); - await cache.WarmUpAsync(ct); - return true; + logger.LogInformation("Priming cache..."); + await cache.GetStringAsync("preload-key", ct); + return true; // Keep hosting if every handler returns true }); -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => -{ - Console.WriteLine("Initializing database connection pool"); - var db = services.GetRequiredService(); - await db.InitializeAsync(ct); - return true; -}); +lambda.MapHandler(([Event] Request request) => new Response("OK")); -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => -{ - Console.WriteLine("Preloading configuration"); - var config = services.GetRequiredService(); - await config.LoadAsync(ct); - return true; -}); +await lambda.RunAsync(); ``` -**Execution**: All three handlers run **concurrently** for faster cold starts. - -### OnInit with Dependency Injection - -OnInit handlers can inject any registered service: +**OnInit characteristics** -```csharp title="Program.cs" -lambda.OnInit(async ( - ICache cache, - IDatabase database, - ILogger logger, - CancellationToken ct -) => -{ - logger.LogInformation("Starting initialization"); - - await Task.WhenAll( - cache.WarmUpAsync(ct), - database.InitializeAsync(ct) - ); +- Runs once per execution environment and shares time with your first invocation. +- Each handler receives a linked `CancellationToken` that cancels when `InitTimeout` elapses or the host stops (default 5 seconds). +- You may register multiple handlers; aws-lambda-host runs them **concurrently** via `Task.WhenAll`. +- Returning a `bool`/`Task` is optional. If you return a value it controls whether the cold start continues (`true`) or aborts (`false`); if you return `void`/`Task`, aws-lambda-host assumes success. +- Exceptions are aggregated. If any handler throws, the framework logs all failures and aborts initialization. - logger.LogInformation("Initialization complete"); - return true; -}); -``` - -### Handling OnInit Failures - -If any handler returns `false`, initialization aborts: +### Handling Failure and Timeouts ```csharp title="Program.cs" -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +lambda.OnInit(async (IConfigLoader config, ILogger logger, CancellationToken ct) => { - var config = services.GetRequiredService(); - try { await config.LoadAsync(ct); - return true; // Success + return true; + } + catch (OperationCanceledException) + { + logger.LogError("Config warmup timed out before InitTimeout."); + return false; } catch (Exception ex) { - Console.Error.WriteLine($"Failed to load configuration: {ex.Message}"); - return false; // Abort initialization + logger.LogCritical(ex, "Failed to prepare configuration."); + return false; } }); ``` -**Result**: Lambda reports initialization failure, preventing invocations. +If any handler returns `false`, AWS receives an initialization failure and never routes traffic to that execution environment. -### OnInit Timeout +### Clearing Lambda Output Formatting -Configure OnInit timeout using `LambdaHostOptions`: +The .NET Lambda runtime captures console output and re-hydrates it into Lambda platform logs. When you prefer to emit your own structured logs (Serilog, MEL JSON, etc.), enable `ClearLambdaOutputFormatting` so an OnInit handler executes before the first invocation: ```csharp title="Program.cs" builder.Services.ConfigureLambdaHostOptions(options => { - options.InitTimeout = TimeSpan.FromSeconds(10); // Default: 5 seconds + options.ClearLambdaOutputFormatting = true; }); +``` -var lambda = builder.Build(); +You can toggle the same setting from configuration (e.g., `appsettings.json` or environment variables) via the `LambdaHostOptions` binding. -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => +## OnShutdown: Container Teardown Hooks + +```csharp title="Program.cs" +lambda.OnShutdown(async (ITelemetrySink telemetry, ILogger logger, CancellationToken ct) => { - // This cancellation token will fire after 10 seconds try { - await LongRunningInitializationAsync(ct); - return true; + logger.LogInformation("Flushing telemetry before shutdown."); + await telemetry.FlushAsync(ct); } - catch (OperationCanceledException) + catch (Exception ex) { - Console.Error.WriteLine("Initialization timed out"); - return false; + logger.LogWarning(ex, "Telemetry flush failed; continuing shutdown."); } }); ``` -## OnShutdown Handlers - -### Basic OnShutdown - -```csharp title="Program.cs" -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => -{ - Console.WriteLine("Shutting down Lambda function..."); - - var cache = services.GetRequiredService(); - await cache.FlushAsync(ct); -}); - -lambda.MapHandler(([Event] Request request) => new Response("Success")); +**OnShutdown characteristics** -await lambda.RunAsync(); -``` +- Runs once per execution environment when AWS sends SIGTERM or the application host stops. +- `HostOptionsPostConfiguration` sets `HostOptions.ShutdownTimeout` to `ShutdownDuration - ShutdownDurationBuffer`, so the `CancellationToken` you receive will fire after that window (500ms - 50ms by default). +- Multiple handlers execute **concurrently**. Each runs inside its own scope so scoped dependencies remain valid even though the invocation pipeline is idle. +- Exceptions are aggregated and rethrown from `StopAsync`. Use structured logging inside handlers to capture root causes before the host tears down. -**No return value required** – handlers execute and complete. - -### Multiple OnShutdown Handlers - -Register multiple shutdown handlers—they execute **sequentially**: +### Multiple Handlers ```csharp title="Program.cs" -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => { - Console.WriteLine("1. Flushing metrics"); - var metrics = services.GetRequiredService(); await metrics.FlushAsync(ct); }); -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => -{ - Console.WriteLine("2. Closing database connections"); - var db = services.GetRequiredService(); - await db.CloseAsync(ct); -}); - -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => +lambda.OnShutdown(async (IDbConnectionPool pool, CancellationToken ct) => { - Console.WriteLine("3. Cleanup complete"); + await pool.DisposeAsync(ct); }); ``` -**Execution**: Handlers run **sequentially** in registration order. +Both handlers are awaited simultaneously. Keep shutdown work small—only the remaining `ShutdownDuration - ShutdownDurationBuffer` window is available. -### OnShutdown with Dependency Injection +## Dependency Scopes and Injection -OnShutdown handlers support dependency injection: +OnInit and OnShutdown handlers support the same source-generated dependency injection experience as middleware and handlers: + +- Request only what you need. The generated delegate resolves typed parameters (services, keyed services, `ILambdaHostContext`, `CancellationToken`, etc.). +- aws-lambda-host creates a new `IServiceScope` for every handler invocation, ensuring scoped services (database units of work, caches) are isolated even though you are outside the invocation pipeline. +- The `IServiceProvider` parameter gives you direct access to the scope for manual resolution when needed. ```csharp title="Program.cs" -lambda.OnShutdown(async ( - IMetrics metrics, - IDatabase database, - ILogger logger, +lambda.OnInit(async ( + IServiceProvider scope, + KeyedService("primary"), CancellationToken ct ) => { - logger.LogInformation("Starting shutdown"); - - // Sequential cleanup - await metrics.FlushAsync(ct); - await database.CloseAsync(ct); - - logger.LogInformation("Shutdown complete"); -}); -``` - -### Handling OnShutdown Errors - -OnShutdown errors are logged but don't prevent shutdown: - -```csharp title="Program.cs" -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => -{ - try - { - var cache = services.GetRequiredService(); - await cache.FlushAsync(ct); - } - catch (Exception ex) - { - Console.Error.WriteLine($"Cache flush failed: {ex.Message}"); - // Shutdown continues despite error - } + await MyWarmupAsync(scope, MyClient, ct); + return true; }); ``` -### OnShutdown Timeout +## Configuring Lifecycle Behavior -Configure shutdown timeout using `LambdaHostOptions`: +Use `ConfigureLambdaHostOptions` to shape lifecycle behavior centrally or bind the same settings from configuration: ```csharp title="Program.cs" builder.Services.ConfigureLambdaHostOptions(options => { - // Time between SIGTERM and SIGKILL - options.ShutdownDuration = TimeSpan.FromMilliseconds(500); // Default: 500ms - - // Buffer to ensure completion before SIGKILL - options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(50); // Default: 50ms -}); - -var lambda = builder.Build(); - -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => -{ - // Cancellation token fires after (500ms - 50ms = 450ms) - try - { - await GracefulShutdownAsync(ct); - } - catch (OperationCanceledException) - { - Console.Error.WriteLine("Shutdown timed out"); - } + options.InitTimeout = TimeSpan.FromSeconds(10); // Cold-start budget + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); // Pre-timeout buffer per invocation + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // SIGTERM→SIGKILL window + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); // Safety margin before SIGKILL + options.ClearLambdaOutputFormatting = true; // Run OnInitClearLambdaOutputFormatting }); ``` -**Shutdown Duration Options:** - -```csharp -// No extension time (0ms) -options.ShutdownDuration = ShutdownDuration.NoExtensions; +**Options summary** -// Internal extensions only (300ms) -options.ShutdownDuration = ShutdownDuration.InternalExtensions; - -// External extensions (500ms) - DEFAULT -options.ShutdownDuration = ShutdownDuration.ExternalExtensions; - -// Custom duration -options.ShutdownDuration = TimeSpan.FromSeconds(2); -``` +- `InitTimeout` – Maximum time all OnInit handlers collectively have before cancellation. +- `InvocationCancellationBuffer` – Buffer subtracted from each invocation's remaining execution time; used by `ILambdaCancellationFactory`. +- `ShutdownDuration` – Expected gap between SIGTERM and SIGKILL (0 ms, 300 ms, 500 ms, or a custom `TimeSpan`). +- `ShutdownDurationBuffer` – Amount deducted from `ShutdownDuration` to guarantee shutdown completes before SIGKILL. +- `ClearLambdaOutputFormatting` – When `true`, automatically registers the built-in OnInit handler that clears Lambda’s console formatting. ## Common Patterns -### Warming Up Caches +### Warm Databases and Caches ```csharp title="Program.cs" -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); -var lambda = builder.Build(); - -lambda.OnInit(async (ICache cache, CancellationToken ct) => +lambda.OnInit(async (IDatabaseSessionFactory factory, CancellationToken ct) => { - Console.WriteLine("Warming up cache..."); - - // Preload frequently accessed data - await cache.SetAsync("config", await LoadConfigAsync(ct), ct); - await cache.SetAsync("lookup", await LoadLookupDataAsync(ct), ct); - - Console.WriteLine("Cache warmed"); + await factory.CreateWarmSessionAsync(ct); return true; }); ``` -### Database Connection Pooling +### Preload Configuration or Secrets ```csharp title="Program.cs" -builder.Services.AddSingleton(); - -var lambda = builder.Build(); - -lambda.OnInit(async (IDatabase db, ILogger logger, CancellationToken ct) => +lambda.OnInit(async (ISecretProvider secrets, ILogger logger, CancellationToken ct) => { - logger.LogInformation("Initializing database connection pool"); - await db.InitializePoolAsync(ct); + await secrets.PreloadAsync(ct); + logger.LogInformation("Secrets cached for fast access."); return true; }); - -lambda.OnShutdown(async (IDatabase db, ILogger logger, CancellationToken ct) => -{ - logger.LogInformation("Closing database connections"); - await db.ClosePoolAsync(ct); -}); ``` -### Telemetry Flushing +### Flush Telemetry on Shutdown ```csharp title="Program.cs" builder.Services.AddSingleton(); -var lambda = builder.Build(); - -lambda.OnShutdown(async (ITelemetry telemetry, ILogger logger, CancellationToken ct) => -{ - logger.LogInformation("Flushing telemetry"); - await telemetry.FlushAsync(ct); -}); -``` - -### Loading ML Models - -```csharp title="Program.cs" -builder.Services.AddSingleton(); - -var lambda = builder.Build(); - -lambda.OnInit(async (IModelService models, ILogger logger, CancellationToken ct) => -{ - logger.LogInformation("Loading ML model"); - await models.LoadAsync("model-v1.0", ct); - logger.LogInformation("Model loaded"); - return true; -}); -``` - -### Configuration Preloading - -```csharp title="Program.cs" -builder.Services.AddSingleton(); - -var lambda = builder.Build(); - -lambda.OnInit(async (IConfigService config, ILogger logger, CancellationToken ct) => -{ - logger.LogInformation("Preloading configuration from SSM"); - - try - { - await config.LoadFromParameterStoreAsync(ct); - logger.LogInformation("Configuration loaded successfully"); - return true; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to load configuration"); - return false; // Abort initialization - } -}); -``` - -## Lifecycle Configuration - -### LambdaHostOptions Reference - -```csharp title="Program.cs" -builder.Services.ConfigureLambdaHostOptions(options => -{ - // OnInit timeout (default: 5 seconds) - options.InitTimeout = TimeSpan.FromSeconds(10); - - // Invocation cancellation buffer (default: 3 seconds) - // Buffer before Lambda timeout to allow graceful cancellation - options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); - - // Shutdown duration (default: 500ms) - options.ShutdownDuration = ShutdownDuration.ExternalExtensions; - - // Shutdown buffer (default: 50ms) - options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); - - // Clear Lambda output formatting (default: false) - options.ClearLambdaOutputFormatting = true; -}); -``` - -### InitTimeout - -Controls how long OnInit handlers can run before cancellation: - -```csharp -builder.Services.ConfigureLambdaHostOptions(options => -{ - options.InitTimeout = TimeSpan.FromSeconds(10); -}); - -lambda.OnInit(async (ICache cache, CancellationToken ct) => -{ - // 'ct' fires after 10 seconds - await cache.WarmUpAsync(ct); - return true; -}); -``` - -### InvocationCancellationBuffer - -Controls when the invocation cancellation token fires relative to Lambda timeout: - -```csharp -builder.Services.ConfigureLambdaHostOptions(options => -{ - // Fire cancellation 5 seconds before Lambda times out - options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); -}); - -lambda.MapHandler(async ([Event] Request request, CancellationToken ct) => -{ - // If Lambda timeout is 30s, 'ct' fires after 25s - await LongRunningOperationAsync(ct); - return new Response("Success"); -}); -``` - -### ShutdownDuration and ShutdownDurationBuffer - -Control shutdown timing: - -```csharp -builder.Services.ConfigureLambdaHostOptions(options => -{ - // Time between SIGTERM and SIGKILL - options.ShutdownDuration = TimeSpan.FromSeconds(1); - - // Buffer to ensure completion - options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); - - // Actual shutdown timeout: 1000ms - 100ms = 900ms -}); - -lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => -{ - // 'ct' fires after 900ms - await metrics.FlushAsync(ct); -}); -``` - -## Best Practices - -### ✅ Do: Keep OnInit Fast - -```csharp -// GOOD: Concurrent initialization -lambda.OnInit(async (ICache cache, CancellationToken ct) => -{ - await cache.WarmUpAsync(ct); - return true; -}); - -lambda.OnInit(async (IDatabase db, CancellationToken ct) => -{ - await db.InitializeAsync(ct); - return true; -}); - -// Both run concurrently - faster cold start -``` - -### ❌ Don't: Perform Slow Sequential Operations - -```csharp -// BAD: Sequential initialization -lambda.OnInit(async (IServiceProvider services, CancellationToken ct) => -{ - var cache = services.GetRequiredService(); - await cache.WarmUpAsync(ct); // Wait... - - var db = services.GetRequiredService(); - await db.InitializeAsync(ct); // Then wait again... - - return true; -}); -``` - -### ✅ Do: Use CancellationToken in OnInit - -```csharp -// GOOD: Respects timeout -lambda.OnInit(async (IConfigService config, CancellationToken ct) => -{ - try - { - await config.LoadAsync(ct); - return true; - } - catch (OperationCanceledException) - { - Console.Error.WriteLine("Initialization timed out"); - return false; - } -}); -``` - -### ✅ Do: Flush Telemetry in OnShutdown - -```csharp -// GOOD: Ensure metrics are sent lambda.OnShutdown(async (ITelemetry telemetry, CancellationToken ct) => { await telemetry.FlushAsync(ct); }); ``` -### ❌ Don't: Ignore OnShutdown Errors - -```csharp -// BAD: Silent failure -lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => -{ - await metrics.FlushAsync(ct); // What if this fails? -}); -``` - -**Better:** - -```csharp -// GOOD: Log errors -lambda.OnShutdown(async (IMetrics metrics, ILogger logger, CancellationToken ct) => -{ - try - { - await metrics.FlushAsync(ct); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to flush metrics"); - } -}); -``` - -### ✅ Do: Return false on Critical OnInit Failures - -```csharp -// GOOD: Abort initialization on critical failure -lambda.OnInit(async (IDatabase db, ILogger logger, CancellationToken ct) => -{ - try - { - await db.ConnectAsync(ct); - return true; - } - catch (Exception ex) - { - logger.LogCritical(ex, "Failed to connect to database"); - return false; // Prevent invocations - } -}); -``` - -### ✅ Do: Use Singleton Services for OnInit/OnShutdown - -```csharp -// GOOD: Singleton persists across invocations -builder.Services.AddSingleton(); - -lambda.OnInit(async (ICache cache, CancellationToken ct) => -{ - await cache.WarmUpAsync(ct); - return true; -}); - -lambda.OnShutdown(async (ICache cache, CancellationToken ct) => -{ - await cache.FlushAsync(ct); -}); - -// Same instance used in OnInit, invocations, and OnShutdown -``` - -## Anti-Patterns to Avoid - -### ❌ Blocking Async Code - -```csharp -// BAD: Blocking async operations -lambda.OnInit((ICache cache, CancellationToken ct) => -{ - cache.WarmUpAsync(ct).Wait(); // DON'T! - return Task.FromResult(true); -}); -``` - -**Better:** - -```csharp -// GOOD: Proper async/await -lambda.OnInit(async (ICache cache, CancellationToken ct) => -{ - await cache.WarmUpAsync(ct); - return true; -}); -``` - ---- - -### ❌ Ignoring CancellationToken - -```csharp -// BAD: Ignoring timeout signal -lambda.OnInit(async (IConfigService config, CancellationToken ct) => -{ - await Task.Delay(TimeSpan.FromMinutes(5)); // Ignores 'ct'! - return true; -}); -``` - -**Better:** - -```csharp -// GOOD: Respect cancellation -lambda.OnInit(async (IConfigService config, CancellationToken ct) => -{ - await Task.Delay(TimeSpan.FromSeconds(3), ct); - return true; -}); -``` - ---- - -### ❌ Heavy Work in OnShutdown - -```csharp -// BAD: Too much work during shutdown -lambda.OnShutdown(async (IServiceProvider services, CancellationToken ct) => -{ - var processor = services.GetRequiredService(); - - // Processing 10,000 records during shutdown? No! - await processor.ProcessAllPendingRecordsAsync(ct); -}); -``` - -**Better:** - -```csharp -// GOOD: Minimal cleanup only -lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => -{ - // Just flush metrics - await metrics.FlushAsync(ct); -}); -``` - ---- - -### ❌ Returning false on Non-Critical Failures - -```csharp -// BAD: Aborting initialization for minor issues -lambda.OnInit(async (ICache cache, CancellationToken ct) => -{ - try - { - await cache.WarmUpAsync(ct); - return true; - } - catch - { - // Cache warming failed, but Lambda can still work! - return false; // Don't abort for this - } -}); -``` - -**Better:** - -```csharp -// GOOD: Continue even if cache warming fails -lambda.OnInit(async (ICache cache, ILogger logger, CancellationToken ct) => -{ - try - { - await cache.WarmUpAsync(ct); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Cache warm-up failed, continuing anyway"); - } - - return true; // Continue initialization -}); -``` - -## Testing Lifecycle Handlers - -### Testing OnInit - -```csharp title="Tests/OnInitTests.cs" -using Xunit; -using NSubstitute; -using Microsoft.Extensions.DependencyInjection; - -public class OnInitTests -{ - [Fact] - public async Task OnInit_WarmsUpCache_ReturnsTrue() - { - // Arrange - var cache = Substitute.For(); - var services = new ServiceCollection() - .AddSingleton(cache) - .BuildServiceProvider(); - - var cts = new CancellationTokenSource(); - - // Act - var result = await OnInitHandler(services, cts.Token); - - // Assert - Assert.True(result); - await cache.Received(1).WarmUpAsync(cts.Token); - } - - private async Task OnInitHandler(IServiceProvider services, CancellationToken ct) - { - var cache = services.GetRequiredService(); - await cache.WarmUpAsync(ct); - return true; - } -} -``` - -### Testing OnShutdown - -```csharp title="Tests/OnShutdownTests.cs" -using Xunit; -using NSubstitute; -using Microsoft.Extensions.DependencyInjection; - -public class OnShutdownTests -{ - [Fact] - public async Task OnShutdown_FlushesMetrics() - { - // Arrange - var metrics = Substitute.For(); - var services = new ServiceCollection() - .AddSingleton(metrics) - .BuildServiceProvider(); - - var cts = new CancellationTokenSource(); - - // Act - await OnShutdownHandler(services, cts.Token); - - // Assert - await metrics.Received(1).FlushAsync(cts.Token); - } - - private async Task OnShutdownHandler(IServiceProvider services, CancellationToken ct) - { - var metrics = services.GetRequiredService(); - await metrics.FlushAsync(ct); - } -} -``` - -## Key Takeaways +## Best Practices -1. **OnInit** – Runs once on cold start for resource initialization -2. **OnShutdown** – Runs once before termination for cleanup -3. **Multiple Handlers** – OnInit runs concurrently, OnShutdown runs sequentially -4. **Return Values** – OnInit returns `true`/`false`; OnShutdown has no return value -5. **CancellationToken** – Always respect timeout signals -6. **Configuration** – Use `LambdaHostOptions` to configure timeouts -7. **DI Support** – Both phases support dependency injection -8. **Keep It Fast** – Minimize cold start time by keeping OnInit lean +- Keep OnInit lean and parallel. Split heavy work into separate handlers so they can run concurrently. +- Always observe the provided `CancellationToken`. Respecting cancellation is the difference between graceful shutdown and a forced SIGKILL. +- Only return `false` from OnInit for truly fatal issues (missing configuration, corrupt state). Log-and-continue for non-critical failures. +- Use `ClearLambdaOutputFormatting` when you emit structured logs and want complete control over console output. +- Run diagnostics or telemetry flushes in OnShutdown, but avoid work that exceeds the remaining window. +- Keep supporting types (record definitions, helper classes) at the bottom of `Program.cs` to make the lifecycle wiring easy to read. ## Next Steps -Now that you understand lifecycle management, explore related topics: - -- **[Dependency Injection](/guides/dependency-injection.md)** – Inject services into lifecycle handlers -- **[Configuration](/guides/configuration.md)** – Configure lifecycle timeouts -- **[Error Handling](/guides/error-handling.md)** – Handle errors in lifecycle handlers -- **[Testing](/guides/testing.md)** – Test lifecycle handlers in isolation -- **[Handler Registration](/guides/handler-registration.md)** – Understand the invocation phase - ---- - -Congratulations! You now understand how to control Lambda lifecycle phases for optimal performance and resource management. +- [Dependency Injection](dependency-injection.md) – Understand scoped lifetimes, keyed services, and context access from lifecycle handlers. +- [Middleware](middleware.md) – Build pipelines that operate during the invocation phase using the same DI primitives. +- [Configuration](../getting-started/core-concepts.md) – Review how lifecycle settings integrate with envelopes, handlers, and host options. From d201a623e8f013d8652ceaec204ec4373805538a Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 12:54:19 -0500 Subject: [PATCH 47/65] feat(docs): remove outdated testing resources and examples for clarity - Deleted temporary resources section with outdated links and references in testing guide. - Removed unstructured testing examples and placeholder content for future updates. - Streamlined the guide format by focusing on relevant and practical testing topics. --- docs/guides/testing.md | 48 ------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/docs/guides/testing.md b/docs/guides/testing.md index adf46e80..6d157220 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -11,51 +11,3 @@ - Mocking AWS Lambda context - Best practices and patterns ---- - -## Temporary Resources - -While this guide is being developed, you can refer to: - -- **[CONTRIBUTING.md](https://github.com/j-d-ha/aws-lambda-host/blob/main/CONTRIBUTING.md)** - Testing patterns and conventions used in the project -- **[Example test files](https://github.com/j-d-ha/aws-lambda-host/tree/main/tests/AwsLambda.Host.UnitTests)** - Real test examples from the framework -- **[AutoNSubstituteData pattern](https://github.com/j-d-ha/aws-lambda-host/blob/main/tests/AwsLambda.Host.UnitTests/AutoNSubstituteDataAttribute.cs)** - Test data generation attribute - ---- - -## Quick Testing Example - -```csharp title="OrderServiceTests.cs" linenums="1" -using NSubstitute; -using Xunit; - -public class OrderServiceTests -{ - [Fact] - public async Task ProcessAsync_ValidOrder_ReturnsSuccess() - { - // Arrange - var repository = Substitute.For(); - repository.SaveAsync(Arg.Any()) - .Returns(new SaveResult { Success = true }); - - var service = new OrderService(repository); - var order = new Order("123", 99.99m); - - // Act - var result = await service.ProcessAsync(order); - - // Assert - Assert.True(result.Success); - await repository.Received(1).SaveAsync(order); - } -} -``` - ---- - -## Next Steps - -- **[Deployment](deployment.md)** - Deploy your tested Lambda functions -- **[Error Handling](error-handling.md)** - Test error scenarios -- **[Handler Registration](handler-registration.md)** - Understand handler patterns for testing From 75dfd9ffce35c163c475b7c9ef2cf44a2e4de5c6 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:15:17 -0500 Subject: [PATCH 48/65] feat(docs): simplify and enhance handler registration guide - Condensed sections on handler mapping and `[Event]` attribute usage for better clarity. - Replaced redundant examples with concise, practical use cases. - Updated DI usage examples with relevant scenarios for services and keyed resolution. - Improved documentation structure with streamlined sections on serialization and lifecycle support. - Enhanced examples with logging, cancellation tokens, and best practices for better readability. --- docs/guides/handler-registration.md | 697 ++++------------------------ docs/guides/lifecycle-management.md | 8 +- 2 files changed, 99 insertions(+), 606 deletions(-) diff --git a/docs/guides/handler-registration.md b/docs/guides/handler-registration.md index 491d4bfc..7062668b 100644 --- a/docs/guides/handler-registration.md +++ b/docs/guides/handler-registration.md @@ -1,687 +1,180 @@ # Handler Registration -Handler registration is the core of aws-lambda-host. The `MapHandler` method combined with the -`[Event]` attribute provides type-safe, reflection-free handler registration with automatic -dependency injection—all powered by compile-time source generation. +`MapHandler` is the entry point for telling `AwsLambda.Host` which delegate should process events. The call looks like an ordinary lambda registration, but source generators intercept it at compile time to wire up serialization, dependency injection, and middleware without reflection. -## Introduction +## Registering a Handler -Unlike traditional AWS Lambda handlers that rely on reflection and method naming conventions, -aws-lambda-host uses source generators and interceptors to analyze your handler at compile time, -generating optimized code with zero runtime overhead. - -**Benefits:** - -- ✅ **Zero reflection** – All parameter resolution happens at compile time -- ✅ **Type-safe** – Compiler errors for missing services or incorrect signatures -- ✅ **AOT ready** – Full support for Native AOT compilation -- ✅ **Better trimming** – Only required dependencies are included -- ✅ **Faster execution** – No reflection means faster cold starts - -## The MapHandler Method - -### Basic Handler - -The simplest handler takes an event parameter marked with `[Event]` and returns a response: - -```csharp title="Program.cs" -using AwsLambda.Host; - -var builder = LambdaApplication.CreateBuilder(); -var lambda = builder.Build(); - -lambda.MapHandler(([Event] string input) => $"Hello, {input}!"); - -await lambda.RunAsync(); -``` - -**How it works:** - -1. Source generator analyzes the handler signature -2. Generates deserialization code for `string` input -3. Generates serialization code for `string` output -4. Creates invocation wrapper—all at compile time - -### Handler with Services - -Inject registered services alongside the event: - -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" using AwsLambda.Host; using Microsoft.Extensions.DependencyInjection; var builder = LambdaApplication.CreateBuilder(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); var lambda = builder.Build(); -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.Process(order) +lambda.MapHandler(([Event] string name, IGreetingService greetings) => + greetings.Greet(name) ); await lambda.RunAsync(); -``` - -**Source generation resolves:** -- `[Event] Order order` → Deserialized from Lambda event -- `IOrderService service` → Resolved from DI container +sealed record GreetingResponse(string Message); -### Async Handlers - -Use `async` handlers for I/O-bound operations: - -```csharp title="Program.cs" -lambda.MapHandler(async ([Event] Order order, IOrderRepository repo) => +sealed class GreetingService : IGreetingService { - var result = await repo.SaveAsync(order); - return new OrderResponse(result.Id, true); -}); -``` - -**Always prefer `async` for:** - -- Database operations -- HTTP requests -- File I/O -- Any awaitable operation - -## The [Event] Attribute - -The `[Event]` attribute marks the parameter that receives the deserialized Lambda event payload. - -### Purpose - -- **Identifies the event parameter** for source generation -- **Triggers code generation** for deserialization -- **Enables type-safe** event handling - -### Rules - -✅ **Required on exactly one parameter** - -```csharp -// GOOD: One [Event] parameter -lambda.MapHandler(([Event] Order order, IOrderService service) => ...); -``` - -❌ **Cannot be omitted** - -```csharp -// BAD: Missing [Event] attribute -lambda.MapHandler((Order order, IOrderService service) => ...); -// Compiler error: No event parameter marked with [Event] -``` - -❌ **Cannot mark multiple parameters** + public GreetingResponse Greet(string name) => new($"Hello, {name}!"); +} -```csharp -// BAD: Multiple [Event] attributes -lambda.MapHandler(([Event] Order order, [Event] string id) => ...); -// Compiler error: Only one parameter can be marked with [Event] +interface IGreetingService +{ + GreetingResponse Greet(string name); +} ``` -### Valid Event Types +- Register middleware (via `lambda.Use(...)`) before calling `MapHandler`; the handler is always the last piece of the pipeline. +- Only one handler can be mapped. If you call `MapHandler` twice, the generator emits error `LH0001` so you catch the issue before publishing. +- The generated handler feeds into the normal invocation builder, so all middleware, features, and diagnostics apply equally to handlers created via `Handle` or `MapHandler`. -The `[Event]` parameter can be any serializable type: +## Handler Signatures and the `[Event]` Parameter -```csharp -// Primitive types -lambda.MapHandler(([Event] string input) => ...); -lambda.MapHandler(([Event] int number) => ...); +Handlers that receive an incoming payload must identify exactly one parameter with `[Event]`. The generator uses that marker to synthesize deserialization logic (JSON by default, or whatever envelope/serializer is active). If your Lambda does **not** expect input (e.g., scheduled jobs, health checks, etc.), you can omit the `[Event]` attribute entirely—just define a handler with no payload parameter and aws-lambda-host skips the event binding phase. -// Complex types -lambda.MapHandler(([Event] Order order) => ...); -lambda.MapHandler(([Event] OrderRequest request) => ...); +- `[Event]` may appear on reference types, structs, records, collection types, or envelope types such as `ApiGatewayRequestEnvelope`. +- Handlers without payloads can simply omit `[Event]` by not declaring an event parameter at all. +- When you do accept a payload, exactly one parameter must be annotated. Missing or duplicate `[Event]` attributes trigger compile-time diagnostics so you catch signature issues early. -// Collections -lambda.MapHandler(([Event] List orders) => ...); -lambda.MapHandler(([Event] Dictionary data) => ...); - -// Envelopes (with envelope packages) -lambda.MapHandler(([Event] ApiGatewayRequestEnvelope request) => ...); -lambda.MapHandler(([Event] SqsEventEnvelope sqsEvent) => ...); -``` - -## Injectable Parameters - -Handlers can inject multiple types of parameters. - -### All Injectable Types - -```csharp title="Program.cs" -lambda.MapHandler(async ( - [Event] Order order, // Lambda event (required) - IOrderService orderService, // Registered service - ICache cache, // Another registered service - ILambdaHostContext context, // Framework context - CancellationToken cancellationToken // Timeout signal -) => +```csharp title="Program.cs" linenums="1" +// No incoming event required +lambda.MapHandler((ILogger logger) => { - // Access invocation metadata - var requestId = context.Items["RequestId"]; - - // Use cancellation token for timeout handling - var result = await orderService.ProcessAsync(order, cancellationToken); - - return new OrderResponse(result.Id, true); + logger.LogInformation("Heartbeat fired at {Timestamp}", DateTimeOffset.UtcNow); }); ``` -### Parameter Resolution - -| Parameter Type | Description | Resolved From | -|----------------------|----------------------|-----------------------------------------| -| `[Event] T` | Lambda event payload | Deserialized from event JSON | -| `IServiceType` | Registered service | DI container (scoped per invocation) | -| `ILambdaHostContext` | Invocation context | Framework (provided per invocation) | -| `CancellationToken` | Timeout signal | Framework (fires before Lambda timeout) | - -### Parameter Order - -Parameter order doesn't matter—except `[Event]` must be present: - -```csharp -// All valid - order doesn't matter -lambda.MapHandler(([Event] Order order, IOrderService service, CancellationToken ct) => ...); -lambda.MapHandler((IOrderService service, [Event] Order order, CancellationToken ct) => ...); -lambda.MapHandler((CancellationToken ct, [Event] Order order, IOrderService service) => ...); -``` +### Parameter Sources -### Multiple Service Injection +Handlers can mix lambda events with services, context objects, and cancellation tokens. This table shows what the generator knows how to supply: -Inject as many services as needed: +| Parameter | Source | +|--------------------------------------------------|-----------------------------------------------------------------------------------------------------| +| `[Event] T event` | Deserialized from the Lambda payload (or envelope). Optional—only include when the handler expects an input. | +| `IServiceType service` | Resolved from the DI container using the invocation scope. | +| `[FromKeyedServices("key")] IServiceType keyed` | Resolves a keyed service registered with `AddKeyed*`. Keys must be constants supported by the BCL. | +| `ILambdaHostContext context` | Framework context that extends `ILambdaContext`, exposes scoped `ServiceProvider`, `Items`, `Features`, `Properties`, and the invocation `CancellationToken`. | +| `ILambdaContext lambdaContext` | Raw AWS Lambda context for folks that prefer the SDK contract. | +| `CancellationToken cancellationToken` | Cancels when `InvocationCancellationBuffer` elapses before the Lambda timeout. | -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" lambda.MapHandler(async ( [Event] OrderRequest request, - IOrderService orderService, - IInventoryService inventoryService, - IPaymentService paymentService, - INotificationService notificationService, - ILogger logger, + [FromKeyedServices("primary")] IOrderProcessor orderProcessor, + ILambdaHostContext context, CancellationToken ct ) => { - logger.LogInformation("Processing order {OrderId}", request.OrderId); - - var inventoryOk = await inventoryService.CheckAsync(request.Items, ct); - if (!inventoryOk) return new OrderResponse { Success = false, Reason = "Out of stock" }; - - var paymentOk = await paymentService.ChargeAsync(request.Payment, ct); - if (!paymentOk) return new OrderResponse { Success = false, Reason = "Payment failed" }; - - var order = await orderService.CreateAsync(request, ct); - await notificationService.NotifyAsync(order, ct); - - return new OrderResponse { Success = true, OrderId = order.Id }; -}); -``` - -**⚠️ Caution:** Too many dependencies may indicate the handler is doing too much. Consider -delegating to a facade service. - -## Return Types - -Handler return values are automatically serialized to JSON. - -### Serialized Responses - -```csharp title="Program.cs" -// Return simple types -lambda.MapHandler(([Event] string input) => input.ToUpper()); -// Returns: "HELLO" (serialized as JSON string) - -// Return complex types -lambda.MapHandler(([Event] Order order) => - new OrderResponse(order.Id, true) -); -// Returns: {"orderId":"123","success":true} - -// Return collections -lambda.MapHandler(([Event] SearchRequest request, ISearchService search) => - search.Find(request.Query) -); -// Returns: [{"id":"1","name":"Item 1"}, ...] -``` - -### Void Returns - -Handlers can return `void` or `Task` for operations with no response: - -```csharp title="Program.cs" -lambda.MapHandler(([Event] LogEntry entry, ILogger logger) => -{ - logger.LogInformation("Log entry: {Entry}", entry); - // No return value -}); - -// Async void -lambda.MapHandler(async ([Event] Order order, IOrderRepository repo) => -{ - await repo.SaveAsync(order); - // No return value -}); -``` - -**Response:** Empty JSON response or `null` - -### Task vs ValueTask - -Both `Task` and `ValueTask` are supported: - -```csharp -// Task -lambda.MapHandler(async ([Event] Order order, IOrderService service) => - await service.ProcessAsync(order) -); - -// ValueTask - for hot paths -lambda.MapHandler(async ([Event] Order order, IOrderService service) => -{ - ValueTask result = service.ProcessAsync(order); - return await result; -}); -``` - -**Prefer `Task`** for most cases. Use `ValueTask` only for hot paths where allocation matters. - -## Source Generation - -Source generators analyze your handler at compile time and generate optimized code. - -### How It Works - -```csharp title="Program.cs" -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.Process(order) -); -``` - -**Generated code** (simplified): - -```csharp -// Deserialization -var order = JsonSerializer.Deserialize(eventJson); - -// Service resolution -var service = context.ServiceProvider.GetRequiredService(); - -// Invocation -var response = service.Process(order); - -// Serialization -var responseJson = JsonSerializer.Serialize(response); -``` - -**All generated at compile time—zero runtime reflection.** - -### Compile-Time Benefits - -✅ **Compile-time errors** for missing services: - -```csharp -lambda.MapHandler(([Event] Order order, IMissingService service) => ...); -// Compiler error if IMissingService not registered -``` - -✅ **Compile-time errors** for incorrect signatures: - -```csharp -lambda.MapHandler((Order order) => ...); -// Compiler error: Missing [Event] attribute -``` - -✅ **Optimized code generation**: - -```csharp -// Source generator creates optimized path for your exact signature -// No reflection, no dynamic dispatch, no runtime overhead -``` - -### Interceptors - -The framework uses C# 12 interceptors to replace the `MapHandler` call site with generated code: - -```csharp -// Your code -lambda.MapHandler(([Event] Order order) => ...); + context.Items["RequestId"] = context.AwsRequestId; -// Intercepted and replaced with -lambda.MapHandlerInterceptor0(([Event] Order order) => ...); -// Where MapHandlerInterceptor0 is generated with optimized code -``` - -**Result:** Zero-cost abstraction—as if you wrote the optimized code by hand. - -## Handler Patterns - -### Simple CRUD Handler + var response = await orderProcessor.ProcessAsync(request, ct); + context.Properties["OrdersProcessed"] = (int)(context.Properties["OrdersProcessed"] ?? 0) + 1; -```csharp title="Program.cs" -lambda.MapHandler(async ( - [Event] CreateOrderRequest request, - IOrderRepository repo -) => -{ - var order = new Order(request.CustomerId, request.Items, request.Total); - await repo.CreateAsync(order); - return new CreateOrderResponse(order.Id); + return response; }); ``` -### Handler with Validation +`ILambdaHostContext.ServiceProvider` is lazily created for each invocation. Prefer constructor- or parameter-injected services because they participate in disposal automatically, but the scoped provider is available for advanced scenarios. -```csharp title="Program.cs" -lambda.MapHandler(([Event] Order order, IValidator validator) => -{ - var validationResult = validator.Validate(order); - if (!validationResult.IsValid) - { - throw new ValidationException(validationResult.Errors); - } +## Return Values and Serialization - return new OrderResponse(order.Id, true); -}); -``` +The generator also emits serialization code for the delegate's return value. Supported shapes include: -### Handler with Context Access +- Plain values (`T`), including records, arrays, `Stream`, or envelope types. +- `Task` and `ValueTask` for asynchronous responses. +- `Task` or `ValueTask` when no result should be written (Lambda receives `null`). -```csharp title="Program.cs" -lambda.MapHandler(([Event] Request request, ILambdaHostContext context) => -{ - // Store request metadata - context.Items["RequestId"] = request.Id; - context.Items["Timestamp"] = DateTime.UtcNow; - - // Access shared properties - var appVersion = context.Properties["Version"] as string; +By default responses are serialized through the configured `ILambdaSerializer` (System.Text.Json unless you swap it). Envelope packages often provide specialized features that capture the response inside an `IResponseFeature`, so the `ILambdaHostContext` can retrieve or mutate it later. - // Process request... - return new Response("Success"); -}); -``` +## Invocation Scope and Context -### Handler with Cancellation Token +Each invocation receives its own dependency injection scope and `ILambdaHostContext`. Use it to share data across middleware and handlers without introducing service-locator patterns. -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" lambda.MapHandler(async ( - [Event] Order order, - IOrderService service, + [Event] ApiGatewayRequestEnvelope request, + ILambdaHostContext context, + ILogger logger, CancellationToken ct ) => { - try - { - return await service.ProcessAsync(order, ct); - } - catch (OperationCanceledException) - { - // Lambda timeout approaching - return new OrderResponse { Success = false, Reason = "Timeout" }; - } -}); -``` - -### Handler with Envelope + var order = request.Body ?? throw new InvalidOperationException("Missing body."); -```csharp title="Program.cs" -using AwsLambda.Host.Envelopes.ApiGateway; + if (context.TryGetEvent>(out var originalEnvelope)) + logger.LogDebug("HTTP Method {Method}", originalEnvelope.RequestContext.Http.Method); -lambda.MapHandler(([Event] ApiGatewayRequestEnvelope request, ILogger logger) => -{ - logger.LogInformation("Request from IP: {IP}", request.RequestContext.Identity.SourceIp); - - // Access payload - var order = request.Body; + var serviceScope = context.ServiceProvider; + var metrics = serviceScope.GetRequiredService(); + await metrics.AddInvocationAsync(order.Id, ct); - // Process... return new ApiGatewayResponseEnvelope { StatusCode = 200, - Body = new OrderResponse(order.Id, true) + Body = new(order.Id, approved: true) }; }); ``` -### Thin Handler Pattern - -**Best Practice:** Keep handlers thin and delegate to services. - -```csharp title="Program.cs" -// GOOD: Thin handler delegates to service -lambda.MapHandler(([Event] Order order, IOrderProcessor processor) => - processor.ProcessAsync(order) -); -``` - -```csharp title="Services/OrderProcessor.cs" -public class OrderProcessor : IOrderProcessor -{ - private readonly IOrderRepository _repository; - private readonly IInventoryService _inventory; - private readonly IPaymentService _payment; - private readonly ILogger _logger; - - public OrderProcessor( - IOrderRepository repository, - IInventoryService inventory, - IPaymentService payment, - ILogger logger) - { - _repository = repository; - _inventory = inventory; - _payment = payment; - _logger = logger; - } - - public async Task ProcessAsync(Order order) - { - _logger.LogInformation("Processing order {OrderId}", order.Id); - - // Complex business logic here - var inventoryOk = await _inventory.ReserveAsync(order.Items); - if (!inventoryOk) throw new InvalidOperationException("Insufficient inventory"); - - var paymentOk = await _payment.ChargeAsync(order.Payment); - if (!paymentOk) throw new InvalidOperationException("Payment failed"); - - await _repository.SaveAsync(order); - - return new OrderResponse(order.Id, true); - } -} -``` - -**Why?** - -- ✅ Testable (test `OrderProcessor` in isolation) -- ✅ Reusable (use `OrderProcessor` in multiple handlers) -- ✅ Maintainable (business logic separated from handler) - -## Best Practices - -### ✅ Do: Keep Handlers Thin - -```csharp -// GOOD: Handler delegates to service -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.ProcessAsync(order) -); -``` - -### ❌ Don't: Put Business Logic in Handlers - -```csharp -// BAD: Business logic in handler -lambda.MapHandler(async ([Event] Order order, IOrderRepository repo, IInventoryService inventory) => -{ - // 50+ lines of business logic - if (order.Items.Count == 0) throw new ValidationException(); - var inventoryOk = await inventory.CheckAsync(order.Items); - if (!inventoryOk) throw new InvalidOperationException(); - // More logic... - return new OrderResponse(order.Id, true); -}); -``` - -### ✅ Do: Use Async/Await for I/O - -```csharp -// GOOD: Async handler for I/O operations -lambda.MapHandler(async ([Event] Order order, IOrderRepository repo) => - await repo.SaveAsync(order) -); -``` - -### ❌ Don't: Block Async Operations - -```csharp -// BAD: Blocking async code -lambda.MapHandler(([Event] Order order, IOrderRepository repo) => - repo.SaveAsync(order).Result // DON'T! -); -``` - -### ✅ Do: Inject Services, Not Factories - -```csharp -// GOOD: Inject service directly -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.ProcessAsync(order) -); -``` - -### ❌ Don't: Use Service Locator Pattern +- `context.Items` is a per-invocation bag for ad-hoc data. +- `context.Properties` mirrors `lambda.Properties` so you can stash long-lived configuration at startup and read it later. +- `context.Features` exposes the ASP.NET-style feature system documented in the middleware guide; features enable decoupled access to the raw event/request/response without direct DI coupling. -```csharp -// BAD: Service locator anti-pattern -lambda.MapHandler(([Event] Order order, IServiceProvider services) => -{ - var service = services.GetRequiredService(); - return service.ProcessAsync(order); -}); -``` +## Source Generation and Diagnostics -### ✅ Do: Return Strongly-Typed Responses +`MapHandler` is decorated as a C# 12 interceptor target. During compilation the generator: -```csharp -// GOOD: Strongly-typed response -lambda.MapHandler(([Event] Order order) => - new OrderResponse(order.Id, true) -); -``` - -### ❌ Don't: Return Anonymous Types +1. Ensures the project is built with C# 11+ so interceptors are available (otherwise `LH0004`). +2. Verifies there is exactly one handler and, when a payload parameter exists, exactly one `[Event]` annotation. +3. Validates keyed service metadata so the requested key matches the DI container's capabilities (`LH0003` when the key uses an unsupported type such as arrays). +4. Emits a strongly typed `Handle` call that deserializes the payload (if any), resolves services via generated code, sets up features, and serializes the response. -```csharp -// BAD: Anonymous type (harder to test and maintain) -lambda.MapHandler(([Event] Order order) => - new { orderId = order.Id, success = true } -); -``` +At runtime the stub `MapHandler` method would throw if invoked, but the interception step guarantees that never happens. You get ahead-of-time compatible, reflection-free code with compile-time errors if the signature is invalid. -### ✅ Do: Use CancellationToken +## Patterns and Best Practices -```csharp -// GOOD: Respect timeout signal -lambda.MapHandler(async ([Event] Order order, IOrderService service, CancellationToken ct) => - await service.ProcessAsync(order, ct) -); -``` +- Keep handlers thin. Delegate business logic to services so you can test them outside Lambda and reuse them across handlers. +- Respect the provided `CancellationToken`; `AwsLambda.Host` fires it `InvocationCancellationBuffer` before the hard Lambda timeout. +- Prefer strongly typed responses or envelopes instead of anonymous objects—serialization contracts stay predictable and versionable. +- Use `ILambdaHostContext.Features` (e.g., `context.GetEvent()`) to decouple middleware from handlers when you need shared metadata. +- Avoid resolving services manually from `IServiceProvider` unless absolutely necessary. Let the generator inject what you need, or expose a dedicated facade service. +- Prefer referencing a static method on a static class when you want to exercise the handler logic outside of the Lambda host. Mapping a method group (`lambda.MapHandler(MyHandler.HandleAsync);`) makes it trivial to unit test the handler by invoking it directly. ## Troubleshooting -### Handler Not Found Error - -**Error:** - -``` -System.InvalidOperationException: No handler registered -``` - -**Solution:** - -Ensure you call `MapHandler` before `RunAsync`: - -```csharp -lambda.MapHandler(([Event] string input) => ...); -await lambda.RunAsync(); // ✅ -``` - -### Missing [Event] Attribute - -**Error:** - -``` -Compiler error: No parameter marked with [Event] attribute -``` +**`LH0001: Multiple handlers registered`** -**Solution:** +Make sure you call `MapHandler` only once. If you need to branch by trigger type, create separate Lambda projects or use envelope dispatching. -Mark the event parameter with `[Event]`: - -```csharp -lambda.MapHandler(([Event] Order order) => ...); // ✅ -``` +**`LH0002: No parameter marked with [Event]`** -### Service Not Registered - -**Error:** - -``` -System.InvalidOperationException: No service for type 'IOrderService' has been registered -``` +Add a `[Event]` attribute when your handler accepts an input payload. This diagnostic does **not** appear for payload-less handlers because no event parameter is required in that case. -**Solution:** +**`InvalidOperationException: No service for type ... has been registered`** -Register the service before building: +Register dependencies before building the application: -```csharp -builder.Services.AddScoped(); // ✅ +```csharp linenums="1" +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddScoped(); var lambda = builder.Build(); ``` -### Multiple [Event] Attributes - -**Error:** - -``` -Compiler error: Only one parameter can be marked with [Event] -``` - -**Solution:** +**`InvalidOperationException: Unable to resolve service referenced by FromKeyedServicesAttribute`** -Only mark one parameter with `[Event]`: - -```csharp -lambda.MapHandler(([Event] Order order, IOrderService service) => ...); // ✅ -``` - -## Key Takeaways - -1. **MapHandler** – Registers your Lambda handler with type-safe DI -2. **[Event] Attribute** – Marks the event parameter (required on exactly one parameter) -3. **Injectable Parameters** – `[Event] T`, registered services, `ILambdaHostContext`, - `CancellationToken` -4. **Return Types** – Any serializable type or `void`/`Task` -5. **Source Generation** – Zero reflection, compile-time optimization, AOT ready -6. **Thin Handlers** – Delegate business logic to services -7. **Async/Await** – Always use async for I/O operations -8. **CancellationToken** – Handle Lambda timeouts gracefully +Keyed services require .NET 8+ DI. Ensure you registered the keyed instance using `AddKeyed*` and that the key is a supported primitive, enum, `string`, `Type`, or `null`. ## Next Steps -Now that you understand handler registration, explore related topics: - -- **[Dependency Injection](/guides/dependency-injection.md)** – Inject services into handlers -- **[Middleware](/guides/middleware.md)** – Build middleware around handlers -- **[Error Handling](/guides/error-handling.md)** – Handle exceptions in handlers -- **[Testing](/guides/testing.md)** – Test handlers in isolation -- **[Configuration](/guides/configuration.md)** – Configure handler behavior - ---- - -Congratulations! You now understand how to register type-safe Lambda handlers with automatic -dependency injection. +- [Dependency Injection](dependency-injection.md) – Learn how scopes, keyed services, and context injection work under the hood. +- [Middleware](middleware.md) – Compose reusable components that run before and after your handler. +- [Lifecycle Management](lifecycle-management.md) – Initialize resources before the first invocation and dispose them during shutdown. +- [Features](../features/envelopes.md) – Understand envelopes and the feature pipeline when you need event-specific helpers. diff --git a/docs/guides/lifecycle-management.md b/docs/guides/lifecycle-management.md index b716b8b8..88946cd8 100644 --- a/docs/guides/lifecycle-management.md +++ b/docs/guides/lifecycle-management.md @@ -12,7 +12,7 @@ Cold Start → OnInit → Invocation 1..N → OnShutdown → Termination - **Invocation** – Runs for every event in the normal middleware/handler pipeline. Cancellation tokens respect `InvocationCancellationBuffer`. - **OnShutdown** – Runs once when the runtime receives SIGTERM or when the host stops. Used to flush telemetry or close connections. -Both OnInit and OnShutdown execute outside the invocation pipeline, but aws-lambda-host creates a brand-new `IServiceScope` for every handler so you can resolve scoped services safely. +Both OnInit and OnShutdown execute outside the invocation pipeline, but `AwsLambda.Host` creates a brand-new `IServiceScope` for every handler so you can resolve scoped services safely. ## OnInit: Cold Start Hooks @@ -46,8 +46,8 @@ await lambda.RunAsync(); - Runs once per execution environment and shares time with your first invocation. - Each handler receives a linked `CancellationToken` that cancels when `InitTimeout` elapses or the host stops (default 5 seconds). -- You may register multiple handlers; aws-lambda-host runs them **concurrently** via `Task.WhenAll`. -- Returning a `bool`/`Task` is optional. If you return a value it controls whether the cold start continues (`true`) or aborts (`false`); if you return `void`/`Task`, aws-lambda-host assumes success. +- You may register multiple handlers; `AwsLambda.Host` runs them **concurrently** via `Task.WhenAll`. +- Returning a `bool`/`Task` is optional. If you return a value it controls whether the cold start continues (`true`) or aborts (`false`); if you return `void`/`Task`, `AwsLambda.Host` assumes success. - Exceptions are aggregated. If any handler throws, the framework logs all failures and aborts initialization. ### Handling Failure and Timeouts @@ -133,7 +133,7 @@ Both handlers are awaited simultaneously. Keep shutdown work small—only the re OnInit and OnShutdown handlers support the same source-generated dependency injection experience as middleware and handlers: - Request only what you need. The generated delegate resolves typed parameters (services, keyed services, `ILambdaHostContext`, `CancellationToken`, etc.). -- aws-lambda-host creates a new `IServiceScope` for every handler invocation, ensuring scoped services (database units of work, caches) are isolated even though you are outside the invocation pipeline. +- `AwsLambda.Host` creates a new `IServiceScope` for every handler invocation, ensuring scoped services (database units of work, caches) are isolated even though you are outside the invocation pipeline. - The `IServiceProvider` parameter gives you direct access to the scope for manual resolution when needed. ```csharp title="Program.cs" From cefae3c8bf9c916f6a72922c8533998cdbd4308a Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:17:38 -0500 Subject: [PATCH 49/65] feat(docs): enhance handler registration guide with practical code examples - Added comprehensive code samples for `Program.cs` and `Handlers.cs` to clarify `MapHandler` usage. - Included DI and `CancellationToken` handling best practices in `Handlers.HandleAsync` example. - Improved guide structure with formatted examples for easier understanding and direct application. --- docs/guides/handler-registration.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/guides/handler-registration.md b/docs/guides/handler-registration.md index 7062668b..cb3b2d43 100644 --- a/docs/guides/handler-registration.md +++ b/docs/guides/handler-registration.md @@ -148,6 +148,30 @@ At runtime the stub `MapHandler` method would throw if invoked, but the intercep - Avoid resolving services manually from `IServiceProvider` unless absolutely necessary. Let the generator inject what you need, or expose a dedicated facade service. - Prefer referencing a static method on a static class when you want to exercise the handler logic outside of the Lambda host. Mapping a method group (`lambda.MapHandler(MyHandler.HandleAsync);`) makes it trivial to unit test the handler by invoking it directly. +=== "Program.cs" + + ```csharp linenums="1" + lambda.MapHandler(Handlers.HandleAsync); + ``` + +=== "Handlers.cs" + + ```csharp linenums="1" + namespace MyLambda; + + static class Handlers + { + public static async Task HandleAsync( + [Event] Request request, + IService service, + CancellationToken ct + ) + { + return await service.ProcessAsync(request, ct); + } + } + ``` + ## Troubleshooting **`LH0001: Multiple handlers registered`** From b9e7c8a9056c32ae2f9b2c825e0b80845c37ce66 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:30:57 -0500 Subject: [PATCH 50/65] feat(docs): enhance middleware guide with decoupling tips and improved feature examples - Updated middleware composition tips to promote decoupling and reduced DI complexity. - Enhanced `Features` section with examples of ASP.NET-style typed capabilities. - Improved guidance on using `IFeatureProvider` including registration and lazy creation patterns. - Expanded example code to illustrate coherent provider usage and maintainability best practices. --- docs/guides/middleware.md | 61 ++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index c72c02e8..c9df321b 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -4,7 +4,7 @@ runs code before/after the next component, and can short-circuit the pipeline. If you're new to the pattern, skim the [ASP.NET Core middleware overview](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/) first. This guide focuses on Lambda-specific behavior: invocation scopes, feature access, and -composition tips. +composition tips that keep middleware and handlers decoupled without extra DI plumbing. ## Pipeline Basics @@ -74,13 +74,13 @@ Key members: `LambdaHostOptions.InvocationCancellationBuffer`). Pass it to downstream async work. - `Items` – per-invocation storage shared by middleware/handler. - `Properties` – cross-invocation storage. -- `Features` – type-keyed feature objects such as `IEventFeature` - and `IResponseFeature`. +- `Features` – ASP.NET-style typed capabilities such as `IEventFeature` and `IResponseFeature` that let middleware collaborate without injecting each other. -## Inline Middleware Only +## Inline Middleware `UseMiddleware` currently accepts inline delegates. Class-based middleware activators are on the roadmap, -so for now keep middleware logic inside the lambda or extract helper services for reuse. +so for now keep middleware logic inside the lambda or extract helper services (registered in DI) for reuse. +Treat the delegate as the orchestration glue and push heavy lifting into services so the code stays testable. ## Working with Features @@ -119,41 +119,49 @@ Common features: - Middleware can extract values set by handlers (or other middleware) without DI fan-out. - Handlers remain free of middleware-specific dependencies; they just work with the event/response types. -- Custom features are easy to add—Just add an implementation of `IFeatureProvider` and register it. It will then be available to all middleware. +- Custom features are easy to add—register an implementation of `IFeatureProvider` and it becomes available to all middleware. -```csharp title="Custom feature" -public interface ICorrelationFeature -{ - string CorrelationId { get; set; } -} +### Feature Providers in Practice -public sealed class CorrelationFeature : ICorrelationFeature -{ - public string CorrelationId { get; set; } = Guid.NewGuid().ToString(); -} +When `context.Features.Get()` runs, aws-lambda-host walks through every registered `IFeatureProvider` +until one returns the requested feature. Built-in providers handle common cases such as response +serialization. Use the same pattern for your features. -lambda.UseMiddleware(async (context, next) => -{ - var feature = context.Features.Get() ?? new CorrelationFeature(); - - context.Items["CorrelationId"] = feature.CorrelationId; - await next(context); -}); +```csharp title="DefaultResponseFeatureProvider.cs" linenums="1" +using Amazon.Lambda.Core; + +namespace AwsLambda.Host.Core; -public sealed class CorrelationFeatureProvider : IFeatureProvider +/// +/// Provides a default implementation of for Lambda response +/// serialization. This provider is instantiated by source-generated code to handle Lambda response +/// processing using the specified . +/// +public class DefaultResponseFeatureProvider(ILambdaSerializer lambdaSerializer) + : IFeatureProvider { - private static readonly Type FeatureType = typeof(ICorrelationFeature); + // ReSharper disable once StaticMemberInGenericType + private static readonly Type FeatureType = typeof(IResponseFeature); + /// public bool TryCreate(Type type, out object? feature) { - feature = type == FeatureType ? new CorrelationFeature() : null; + feature = type == FeatureType ? new DefaultResponseFeature(lambdaSerializer) : null; + return feature is not null; } } +``` -builder.Services.AddSingleton(); +Registering a provider is just another DI call: + +```csharp title="Program.cs" +builder.Services.AddSingleton(); // implements IFeatureProvider ``` +Your provider can return singleton instances (for stateless metadata) or create fresh objects per +invocation. + ## Short-Circuiting and Error Handling Middleware can stop the pipeline early: @@ -227,7 +235,6 @@ or services via `context.ServiceProvider` the same way you would inside a handle report success unintentionally. - **Use per-invocation state wisely.** `Items` is cleared after each request; `Properties` live for the life of the container and must be thread-safe. -- **Prefer class-based middleware** for anything reusable or needing DI/Options. - **Make cancellation cooperative.** Honor `context.CancellationToken` in middleware and pass it to downstream I/O. With these patterns, you can build rich, testable pipelines around your Lambda handlers while keeping From 2deb4db70958a0b4e4d6613899903e8ccd4685f3 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:31:29 -0500 Subject: [PATCH 51/65] feat(docs): update guides with placeholders and enhanced examples - Added spacing to improve readability in the Topics section. - Updated `Additional Resources` links with "Coming Soon" notes for clarity on upcoming content. - Enhanced descriptions for `Examples` and `Advanced Topics` with detailed placeholders. - Improved formatting consistency across the `index.md` guide. --- docs/guides/index.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/guides/index.md b/docs/guides/index.md index 7a943d49..eae14790 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -21,6 +21,7 @@ Learn service registration patterns, understand Singleton vs Scoped lifetimes, a Build middleware pipelines for cross-cutting concerns like logging, metrics, validation, and error handling. **Topics covered:** + - Middleware pipeline composition - Common middleware patterns - Context and state management @@ -106,9 +107,9 @@ After mastering the guides, explore [Advanced Topics](/advanced/) for AOT compil ## Additional Resources -- **[Examples](/examples/)** – Repository sample projects (more coming soon) -- **[Features](/features/)** – Envelope packages and OpenTelemetry add-ons -- **[Advanced Topics](/advanced/)** – Placeholder for Native AOT, generators, and performance deep dives +- **[Examples (Coming Soon)](/examples/)** – Guided sample apps covering middleware, envelopes, and DI wiring. +- **[Features](/features/)** – Envelope packages and OpenTelemetry add-ons. +- **[Advanced Topics (Coming Soon)](/advanced/)** – Native AOT, generator internals, and performance deep dives. ## Getting Help From 79327f7d7419114224bd3b497b5bd9546a414e60 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:32:29 -0500 Subject: [PATCH 52/65] fix(docs): correct typo in Advanced Topics content - Fixed typo in the mention of "source generator internals" for consistency and clarity. --- docs/advanced/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/index.md b/docs/advanced/index.md index 793b3ec5..8df0e409 100644 --- a/docs/advanced/index.md +++ b/docs/advanced/index.md @@ -1,5 +1,5 @@ # Advanced Topics -> Coming soon: guidance on Native AOT, source generators internals, and performance tuning for aws-lambda-host. +> Coming soon: guidance on Native AOT, source generator internals, and performance tuning for aws-lambda-host. We're working on detailed content for advanced scenarios. In the meantime, follow the repository's CHANGELOG and GitHub discussions for early tips. From 31040e1f342c9c2ec596055aa22ab293043baa15 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:32:45 -0500 Subject: [PATCH 53/65] fix(docs): update outdated timestamps in examples for consistency - Replaced future timestamps with realistic past dates in response examples for clarity. - Ensured consistency across similar example JSON structures in the guide. --- docs/getting-started/first-lambda.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md index 2bf3dbb9..efb2a572 100644 --- a/docs/getting-started/first-lambda.md +++ b/docs/getting-started/first-lambda.md @@ -307,7 +307,7 @@ Click **Execute Function** and verify the response: ```json title="Expected Response" { "message": "Hello, World!", - "timestamp": "2025-11-29T12:34:56.789Z" + "timestamp": "2016-08-12T07:24:18.456Z" } ``` @@ -525,7 +525,7 @@ Expected output in `response.json`: ```json { "message": "Hola, World!", - "timestamp": "2025-11-29T12:34:56.789Z" + "timestamp": "2016-08-12T07:24:18.456Z" } ``` From f9cec871a4e03ec3218147872f64f8528de95c7b Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:34:20 -0500 Subject: [PATCH 54/65] feat(docs): streamline core concepts and lifecycle guide - Condensed lifecycle phase explanations for improved clarity and focus. - Simplified examples for `OnInit`, `Invocation`, and `OnShutdown` phases with practical scenarios. - Updated diagrams and flowcharts for consistency with the new lifecycle structure. - Enhanced dependency injection section with a lifetime comparison table and concise explanations. - Improved middleware and feature usage examples with structured logging and reduced redundancy. --- docs/getting-started/core-concepts.md | 875 +++++--------------------- mkdocs.yml | 4 +- 2 files changed, 157 insertions(+), 722 deletions(-) diff --git a/docs/getting-started/core-concepts.md b/docs/getting-started/core-concepts.md index 63ea12c7..c25b4dce 100644 --- a/docs/getting-started/core-concepts.md +++ b/docs/getting-started/core-concepts.md @@ -1,94 +1,87 @@ # Core Concepts -This guide explores the fundamental concepts that power aws-lambda-host: the Lambda lifecycle, dependency injection, middleware pipelines, handler registration, and source generation. Understanding these concepts will help you build robust, maintainable Lambda functions. +This guide explains how aws-lambda-host stitches together the Lambda lifecycle, dependency injection, middleware, and source generation. Knowing these building blocks makes it easier to reason about performance, testing, and extensibility as your functions grow. -## Lambda Lifecycle +## The Lambda Lifecycle -AWS Lambda functions go through three distinct phases during their execution. Understanding these phases is crucial for optimizing performance and managing resources effectively. +An execution environment progresses through distinct stages. aws-lambda-host exposes each one so you can opt into work at the right time. -### The Three Phases +``` +Cold Start → OnInit → Invocation 1..N → OnShutdown → Termination +``` -#### 1. OnInit Phase (Cold Start) +### OnInit (Cold Start) -The OnInit phase runs **once** when a Lambda container starts (cold start). This is your opportunity to perform expensive setup operations that can be reused across multiple invocations. +OnInit handlers run once when AWS boots a new execution environment. **Characteristics:** -- Runs only once per container lifecycle -- Singleton services are created during this phase -- Exceptions here prevent the Lambda from starting -- Should complete quickly to minimize cold start time +- Executes before the first invocation shares its CPU slice, so keep work short. +- Each handler receives its own scoped `IServiceProvider`, even though the invocation pipeline has not started yet. +- Multiple handlers run concurrently via `Task.WhenAll`. +- Returning `bool`/`Task` is optional. If you return a value, `false` tells the host to abort startup; omitting the return value (or returning `Task`) implies success. +- Exceptions are aggregated and bubble to AWS so the container never serves traffic. -**Perfect for:** +**Great for:** -- Opening database connections -- Loading configuration -- Warming caches -- Initializing HTTP clients -- Setting up telemetry +- Hydrating caches or distributed clients. +- Clearing Lambda console formatting via `LambdaHostOptions.ClearLambdaOutputFormatting` when you use your own logging pipeline. +- Running migrations or priming configuration secrets. -**Example:** - -```csharp -lambda.OnInit(async (services, cancellationToken) => +```csharp title="Program.cs" linenums="1" +lambda.OnInit(async ( + ICache cache, + ILogger logger, + CancellationToken ct +) => { - var cache = services.GetRequiredService(); - await cache.WarmupAsync(cancellationToken); - Console.WriteLine("Cache warmed up successfully"); - return true; // Return false to abort startup + logger.LogInformation("Warming cache before first invocation"); + await cache.PopulateAsync(ct); + return true; // omit when you do not need to signal failure explicitly }); ``` -#### 2. Invocation Phase - -The Invocation phase runs **for each Lambda event**. This is where your business logic executes. - -**Characteristics:** +### Invocation Pipeline -- Runs for every invocation -- New DI scope created per invocation -- Scoped services are instantiated -- Handler executes with injected dependencies -- Exceptions are returned as error responses +Every event runs through the middleware pipeline and handler you registered with `MapHandler`. aws-lambda-host creates a scoped service provider, sets up features, and links the cancellation token to the Lambda timeout. -**What happens:** +**Invocation steps:** -1. Lambda Runtime receives event -2. Event is deserialized to your model -3. New DI scope created -4. Middleware pipeline executes -5. Handler executes -6. Response serialized to JSON -7. DI scope disposed +1. Lambda runtime delivers the JSON payload. +2. The generated handler deserializes it (if you marked a parameter with `[Event]`). +3. aws-lambda-host creates a scoped `IServiceProvider` for the invocation. +4. Middleware executes in the order it was registered. +5. Your handler runs and can resolve services/contexts from DI. +6. The response is captured through `IResponseFeature` and serialized via the configured `ILambdaSerializer`. +7. The invocation scope is disposed. -#### 3. OnShutdown Phase +### OnShutdown (Teardown) -The OnShutdown phase runs **once** before the Lambda container terminates. Use this for cleanup and flushing data. +AWS sends SIGTERM before reclaiming the execution environment. OnShutdown handlers allow you to flush telemetry or dispose expensive resources. **Characteristics:** -- Runs once before container shutdown -- Limited time window (configurable, default 500ms with a 50ms safety buffer) -- Perfect for flushing metrics, logs, or buffers -- Services are still available for injection +- Runs once per environment when shutdown begins (timeout defined by `LambdaHostOptions.ShutdownDuration - ShutdownDurationBuffer`). +- Each handler executes in its own fresh scope, so scoped services remain usable even while no invocation is active. +- Handlers run concurrently; long-running work can still time out if it exceeds the remaining shutdown window. +- Use structured logging inside the handler—any exception is aggregated and rethrown from `StopAsync`. -By default `LambdaHostOptions` sets `ShutdownDuration` to `ShutdownDuration.ExternalExtensions` (500ms) and subtracts `ShutdownDurationBuffer` (50ms). Increase those values if your cleanup needs a longer runway. - -**Perfect for:** - -- Flushing telemetry data -- Closing database connections -- Final cleanup operations -- Logging shutdown events - -**Example:** - -```csharp -lambda.OnShutdown(async (services, cancellationToken) => +```csharp title="Program.cs" linenums="1" +lambda.OnShutdown(async ( + ITelemetrySink telemetry, + ILogger logger, + CancellationToken ct +) => { - var metrics = services.GetRequiredService(); - await metrics.FlushAsync(cancellationToken); - Console.WriteLine("Metrics flushed before shutdown"); + try + { + await telemetry.FlushAsync(ct); + logger.LogInformation("Telemetry flushed before shutdown"); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Telemetry flush failed; continuing shutdown"); + } }); ``` @@ -96,734 +89,176 @@ lambda.OnShutdown(async (services, cancellationToken) => ```mermaid graph LR - A[Cold Start] --> B[OnInit Phase] - B --> C{Init Success?} + A[Cold Start] --> B[OnInit] + B --> C{Init success?} C -->|Yes| D[Ready] - C -->|No| E[Failed] + C -->|No| E[Aborted] D --> F[Invocation 1] F --> G[Invocation 2] G --> H[Invocation 3] H --> I[...] - I --> J[Shutdown Signal] - J --> K[OnShutdown Phase] - K --> L[Terminated] -``` - -### Complete Lifecycle Example - -```csharp -using System; -using AwsLambda.Host.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -var builder = LambdaApplication.CreateBuilder(); - -// Register services -builder.Services.AddSingleton(); -builder.Services.AddScoped(); - -var lambda = builder.Build(); - -// OnInit - runs once on cold start -lambda.OnInit(async (services, cancellationToken) => -{ - Console.WriteLine("=== INIT PHASE START ==="); - var cache = services.GetRequiredService(); - await cache.WarmupAsync(cancellationToken); - Console.WriteLine("=== INIT PHASE COMPLETE ==="); - return true; -}); - -// Handler - runs for each invocation -lambda.MapHandler(([Event] Order order) => -{ - Console.WriteLine($"=== INVOCATION: Order {order.Id} ==="); - return new OrderResponse(order.Id, true); -}); - -// OnShutdown - runs once before termination -lambda.OnShutdown(async (services, cancellationToken) => -{ - Console.WriteLine("=== SHUTDOWN PHASE START ==="); - var cache = services.GetRequiredService(); - await cache.FlushAsync(cancellationToken); - Console.WriteLine("=== SHUTDOWN PHASE COMPLETE ==="); -}); - -await lambda.RunAsync(); -``` - -!!! tip "Lifecycle Timeouts" - You can configure timeout durations using `LambdaHostOptions`: - ```csharp - builder.Services.ConfigureLambdaHostOptions(options => - { - options.InitTimeout = TimeSpan.FromSeconds(10); - options.ShutdownDuration = TimeSpan.FromSeconds(5); - }); - ``` - -## Dependency Injection - -Dependency injection (DI) is a core feature of aws-lambda-host. It enables testable, maintainable code by managing dependencies through a service container. - -### Service Lifetimes - -`aws-lambda-host` uses the standard `Microsoft.Extensions.DependencyInjection` container, so you can register **singleton**, **scoped**, and **transient** services. Lambda's execution model makes singleton and scoped the most common choices, but transients work too when you need a fresh instance every time a dependency is resolved. - -#### Singleton (Container Level) - -Singleton services are created **once** during the OnInit phase and **reused across all invocations**. - -**Characteristics:** -- Created during OnInit -- Shared across all invocations -- Must be thread-safe -- Efficient for shared resources - -**Perfect for:** -- HTTP clients (HttpClient, HttpClientFactory) -- Caches and in-memory stores -- Configuration objects -- Database connection pools -- Stateless services - -**Example:** - -```csharp -builder.Services.AddHttpClient(); // registers IHttpClientFactory + typed clients -builder.Services.AddSingleton(); -builder.Services.AddSingleton(builder.Configuration); + I --> J[SIGTERM] + J --> K[OnShutdown] + K --> L[Termination] ``` -!!! warning "Thread Safety Required" - Singleton services must be thread-safe because they can be accessed by concurrent invocations. - -#### Scoped (Invocation Level) - -Scoped services are created **once per invocation** and **disposed after the invocation completes**. - -**Characteristics:** -- Created when invocation starts -- Isolated between invocations -- Automatically disposed after invocation -- Can maintain per-request state - -**Perfect for:** -- Database contexts (Entity Framework DbContext) -- Repositories with invocation-specific state -- Unit of Work patterns -- Request-specific loggers -- Transaction managers - -**Example:** - -```csharp -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -``` - -#### Transient (Per Resolve) - -Transient services are created **every time** they are requested from the container. +## Dependency Injection Fundamentals -**Characteristics:** -- New instance per resolution (even within the same invocation) -- Great for lightweight, stateless helpers -- Avoid expensive setup logic here—prefer scoped/singleton for that - -**Use carefully for:** -- Simple mappers/formatters -- Stateless validators -- Small wrappers around third-party SDK calls +aws-lambda-host uses the standard `Microsoft.Extensions.DependencyInjection` container. Registrations happen before `builder.Build()`, and the framework aligns service lifetimes with the Lambda lifecycle. -Register them the same way you would in ASP.NET Core: - -```csharp -builder.Services.AddTransient(); -``` +| Lifetime | Created | Disposed | Use for | +|-----------|--------------------------------------|-------------------------------------------|---------------------------------------------| +| Singleton | During OnInit (cold start) | When AWS tears down the execution context | HttpClient, caches, configuration, SDKs | +| Scoped | At the beginning of each invocation | After the invocation completes | DbContext, repositories, per-request state | +| Transient | Each time the service is requested | With the consuming scope | Lightweight helpers, formatters, pure logic | -### DI Scope Visualization - -```mermaid -graph TD - Container[DI Container
Singleton Services] --> Scope1[Invocation 1 Scope
Scoped Services] - Container --> Scope2[Invocation 2 Scope
Scoped Services] - Container --> Scope3[Invocation 3 Scope
Scoped Services] - - Scope1 -.->|Disposed after| Inv1[Invocation 1 Complete] - Scope2 -.->|Disposed after| Inv2[Invocation 2 Complete] - Scope3 -.->|Disposed after| Inv3[Invocation 3 Complete] -``` +**Tips:** -### Injectable Parameters +- Scoped services are the safest default. They do not leak data across invocations. +- Transients behave the same as ASP.NET Core—be mindful of repeated expensive setup. +- Never store scoped services on singletons; resolve them per invocation or pass data explicitly. -Handlers can inject various types of parameters: +### Working with `ILambdaHostContext` -| Parameter Type | Description | Attribute Required | -|---------------|-------------|-------------------| -| Event Model | The Lambda event (your custom type) | `[Event]` | -| Registered Services | Any service registered in DI container | No | -| `ILambdaHostContext` | Framework context with Lambda metadata | No | -| `CancellationToken` | For cancellation and timeout handling | No | +Every middleware component and handler can ask for `ILambdaHostContext`. Think of it as `HttpContext` for Lambda. -If your handler doesn't need an event payload, simply leave the `[Event]` row out of the parameter list and inject only the services you need. +`ILambdaHostContext` exposes: -**Example:** +- `ServiceProvider` – the scoped provider for the current invocation. +- `CancellationToken` – fires `InvocationCancellationBuffer` before the hard Lambda timeout. +- `Items` – per-invocation bag for quick data sharing. +- `Properties` – shared dictionary backed by the singleton container (must be thread-safe). +- `Features` – typed capabilities (just like ASP.NET Core features) for accessing the event, response, or custom behaviors without direct DI coupling. -```csharp +```csharp title="Program.cs" linenums="1" lambda.MapHandler(async ( - [Event] Order order, // Lambda event (required) - IOrderService orderService, // Registered service - ICache cache, // Another service - ILambdaHostContext context, // Framework context - CancellationToken cancellationToken // Cancellation token + [Event] OrderRequest request, + ILambdaHostContext context, + IOrderService service, + CancellationToken ct ) => { - // Access Lambda metadata - context.Items["OrderId"] = order.Id; - context.Items["StartTime"] = DateTime.UtcNow; + context.Items["RequestId"] = context.AwsRequestId; - // Check cache - if (cache.TryGet(order.Id, out var cached)) - return cached; + var response = await service.ProcessAsync(request, ct); + context.Features.Get>()!.Response = response; - // Process with cancellation support - var result = await orderService.ProcessAsync(order, cancellationToken); - - // Update cache - cache.Set(order.Id, result); - - return result; + return response; }); ``` -### Service Registration Patterns - -```csharp -var builder = LambdaApplication.CreateBuilder(); - -// === Singleton Services (shared) === -builder.Services.AddHttpClient(); -builder.Services.AddSingleton(); - -// Singleton with factory -builder.Services.AddSingleton(sp => - new ConnectionPool(sp.GetRequiredService()) -); - -// === Scoped Services (per invocation) === -builder.Services.AddScoped(); -builder.Services.AddScoped(); +### Parameter Sources -// Scoped with interface injection -builder.Services.AddScoped(sp => - new RequestContext(sp.GetRequiredService()) -); +Handlers and lifecycle hooks can request multiple parameter types simultaneously: -// === Configuration === -builder.Services.Configure( - builder.Configuration.GetSection("AppSettings") -); -``` +- `[Event] T event` – Optional marker for the deserialized payload. Include it only when your Lambda expects input; the generator enforces that at most one parameter carries `[Event]`. +- Services – Any registered service, keyed service (`[FromKeyedServices("key")]`), or options type. +- Context – `ILambdaHostContext` or the raw `ILambdaContext` from the AWS SDK. +- `CancellationToken` – Linked to end-to-end timeouts; pass it downstream. ## Middleware Pipeline -Middleware provides a way to compose request processing logic in a pipeline. Each middleware can execute code before and after the next middleware (or handler) in the pipeline. - -### How Middleware Works - -Middleware forms a chain where each component can: - -1. **Pre-process**: Execute logic before the handler -2. **Invoke next**: Call the next middleware/handler -3. **Post-process**: Execute logic after the handler - -### Basic Middleware Pattern - -```csharp -lambda.UseMiddleware(async (context, next) => -{ - // === PRE-PROCESSING === - // Runs BEFORE the handler - Console.WriteLine("Before handler execution"); - var stopwatch = Stopwatch.StartNew(); - - try - { - // === CALL NEXT MIDDLEWARE/HANDLER === - await next(context); - - // === POST-PROCESSING (Success) === - // Runs AFTER the handler (success path) - stopwatch.Stop(); - Console.WriteLine($"Handler succeeded in {stopwatch.ElapsedMilliseconds}ms"); - } - catch (Exception ex) - { - // === POST-PROCESSING (Error) === - // Runs AFTER the handler (error path) - stopwatch.Stop(); - Console.WriteLine($"Handler failed after {stopwatch.ElapsedMilliseconds}ms: {ex.Message}"); - throw; // Re-throw to propagate error - } -}); -``` - -### Multiple Middleware Execution Order - -Middleware executes in a **nested** fashion—like Russian dolls: - -```csharp -// Register middleware in order -lambda.UseMiddleware(/* MW1: Logging */); -lambda.UseMiddleware(/* MW2: Metrics */); -lambda.UseMiddleware(/* MW3: Validation */); -lambda.MapHandler(/* Handler */); -``` - -**Execution flow:** - -```mermaid -sequenceDiagram - participant Request - participant MW1 - participant MW2 - participant MW3 - participant Handler - - Request->>MW1: Enter - Note over MW1: Pre-process - MW1->>MW2: next() - Note over MW2: Pre-process - MW2->>MW3: next() - Note over MW3: Pre-process - MW3->>Handler: next() - Note over Handler: Execute - Handler-->>MW3: Return - Note over MW3: Post-process - MW3-->>MW2: Return - Note over MW2: Post-process - MW2-->>MW1: Return - Note over MW1: Post-process - MW1-->>Request: Response -``` - -### Middleware Use Cases +Middleware composes the invocation pipeline. Register delegates before `MapHandler`; aws-lambda-host wraps them around the handler in the order you specify. -#### 1. Logging Middleware - -```csharp -lambda.UseMiddleware(async (context, next) => -{ - Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request started"); - await next(context); - Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] Request completed"); -}); -``` - -#### 2. Timing/Performance Middleware - -```csharp +```csharp title="Program.cs" linenums="1" lambda.UseMiddleware(async (context, next) => { var stopwatch = Stopwatch.StartNew(); await next(context); - stopwatch.Stop(); - - Console.WriteLine($"Execution time: {stopwatch.ElapsedMilliseconds}ms"); - - if (stopwatch.ElapsedMilliseconds > 1000) - Console.WriteLine("WARNING: Slow execution detected"); -}); -``` - -#### 3. Error Handling Middleware - -```csharp -lambda.UseMiddleware(async (context, next) => -{ - try - { - await next(context); - } - catch (ValidationException ex) - { - Console.WriteLine($"Validation error: {ex.Message}"); - // Handle validation errors - throw; - } - catch (Exception ex) - { - Console.WriteLine($"Unexpected error: {ex}"); - // Log to monitoring service - throw; - } + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogInformation("Handler finished in {Elapsed}ms", stopwatch.ElapsedMilliseconds); }); -``` - -#### 4. Correlation ID Middleware -```csharp lambda.UseMiddleware(async (context, next) => { - var correlationId = Guid.NewGuid().ToString(); - context.Items["CorrelationId"] = correlationId; - Console.WriteLine($"Correlation ID: {correlationId}"); - + var eventFeature = context.Features.Get>(); + context.Items["OrderId"] = eventFeature?.Event?.Id; await next(context); }); -``` - -### Middleware vs Handlers Comparison - -| Feature | Middleware | Handler | -|---------|-----------|---------| -| **Execution** | Every invocation | Single invocation only | -| **Parameters** | `ILambdaHostContext`, `Func next` | `[Event]` + Services | -| **Return Type** | `void` or `Task` | Event response type | -| **Purpose** | Cross-cutting concerns | Business logic | -| **Count** | Multiple (forms pipeline) | One per Lambda | -| **When to Use** | Logging, metrics, validation | Domain logic | - -## Handler Registration -Handlers are where your business logic lives. The framework uses source generation to create optimized, type-safe handlers. - -### Basic Handler - -The simplest handler takes an input and returns an output: - -```csharp -lambda.MapHandler(([Event] string input) => input.ToUpper()); +lambda.MapHandler(([Event] OrderRequest order) => new OrderResponse(order.Id, true)); ``` -### Handler with Services +**Ordering guidance:** -Inject services directly into your handler: +- Register diagnostics (logging, metrics, tracing) first so they wrap everything else. +- Authentication and authorization should precede validation and business logic. +- Short-circuiting middleware (caching, deduplication) belongs near the handler so it sees the final request context. -```csharp -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.Process(order) -); -``` +## Feature System -### Handler with Multiple Dependencies +Features provide decoupled access to invocation data—mirroring ASP.NET Core’s `HttpContext.Features` and Azure Functions’ binding features. Instead of injecting every dependency everywhere, middleware and handlers trade typed capabilities through the `ILambdaHostContext.Features` collection. -Inject as many services as you need: +Built-in feature types include: -```csharp -lambda.MapHandler(([Event] Order order, - IOrderService orderService, - IInventoryService inventoryService, - IPaymentService paymentService) => -{ - // All services are automatically injected - var inventory = inventoryService.CheckAvailability(order); - var payment = paymentService.ProcessPayment(order); - return orderService.FulfillOrder(order, inventory, payment); -}); -``` +- `IEventFeature` – Holds the deserialized payload (or envelope) so middleware can inspect it. +- `IResponseFeature` – Captures or mutates the handler response before serialization. +- `IInvocationDataFeature` – Exposes raw request/response streams for envelope packages. -### Async Handlers +Features are created lazily. When `context.Features.Get()` executes, the framework asks each registered `IFeatureProvider` whether it can build the requested type—similar to ASP.NET Core’s feature providers. -Use async/await for I/O operations: +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.Abstractions.Features; -```csharp -lambda.MapHandler(async ([Event] Order order, - IOrderRepository repository, - CancellationToken cancellationToken) => +lambda.UseMiddleware(async (context, next) => { - var result = await repository.SaveAsync(order, cancellationToken); - return new OrderResponse(result.Id, true); -}); -``` + var logger = context.ServiceProvider.GetRequiredService>(); -### Handler with Context + if (context.Features.Get>() is { Event: { } request }) + logger.LogInformation("Processing order {OrderId}", request.Id); -Access Lambda context information: - -```csharp -lambda.MapHandler(([Event] Request request, ILambdaHostContext context) => -{ - // Store metadata in context items - context.Items["RequestId"] = request.Id; - context.Items["ProcessedAt"] = DateTime.UtcNow; + await next(context); - // Process request... - return new Response("Success"); + var responseFeature = context.Features.Get>(); + if (responseFeature?.Response is { } response) + logger.LogInformation("Order {OrderId} complete", response.OrderId); }); ``` -### The [Event] Attribute - -Use `[Event]` to tell the source generator which parameter should receive the deserialized Lambda payload. Handlers can have zero or one `[Event]` parameters: - -- ✅ Mark exactly one parameter if you want the framework to deserialize the incoming event. -- ✅ Omit the attribute entirely for DI-only handlers (scheduled jobs, Queue pollers, etc.). The parameter list can consist solely of injected services. -- ❌ Do not decorate multiple parameters—the generator emits `LH0002` if you try. - -If you accidentally leave `[Event]` off a parameter that should receive the payload, the framework assumes it's a DI service and no event data is bound, so you'll spot the issue quickly during testing. - -**Examples:** - -```csharp -// Typical case: bind the event payload -lambda.MapHandler(([Event] Order order, IService service) => service.Process(order)); - -// DI-only handler: no event payload required -lambda.MapHandler((IMaintenanceJob job, ILogger logger) => job.RunAsync(logger)); - -// Compile-time error: multiple [Event] attributes -lambda.MapHandler(([Event] Order order, [Event] string id) => { ... }); -``` - -## Source Generation - -aws-lambda-host uses C# source generators and interceptors to optimize Lambda performance at compile time. - -### What Happens at Compile Time - -When you write: - -```csharp -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.Process(order) -); -``` - -The source generator: - -1. **Analyzes** the handler signature during compilation -2. **Validates** that `[Event]` is used correctly -3. **Generates** deserialization code for the Order type -4. **Generates** dependency injection resolution code -5. **Injects** optimized code via interceptors - -### Benefits - -#### Zero Runtime Reflection - -Traditional frameworks use reflection to inspect types and resolve dependencies at runtime. Source generation moves this to compile time. +### Creating Custom Features -**Result**: Faster execution, especially during cold starts. +Register `IFeatureProvider` implementations to add your own features or override the defaults. The framework ships providers such as `DefaultResponseFeatureProvider` (shown below) and envelope-specific factories. -#### AOT Compilation Ready +```csharp title="DefaultResponseFeatureProvider.cs" linenums="1" +using Amazon.Lambda.Core; -Source generation enables Native AOT compilation because all type information is resolved at compile time. +namespace AwsLambda.Host.Core; -**Result**: Smallest possible package size and fastest cold starts. - -#### Compile-Time Errors - -Type mismatches and configuration errors are caught during compilation, not at runtime. - -**Result**: Fewer production bugs and faster feedback during development. - -#### Better Performance - -Generated code is optimized specifically for your handler signature. - -**Result**: Minimal overhead, maximum throughput. - -### Source Generation Example - -Your code: - -```csharp -lambda.MapHandler(([Event] Order order, IOrderService service) => - service.Process(order) -); -``` - -Generated code (simplified): - -```csharp -// Generated by source generator -private static async Task HandleInvocation( - Stream inputStream, - IServiceProvider services) +/// +/// Provides a default implementation of for Lambda response +/// serialization. This provider is instantiated by source-generated code to handle Lambda response +/// processing using the specified . +/// +public class DefaultResponseFeatureProvider(ILambdaSerializer lambdaSerializer) + : IFeatureProvider { - // Deserialize event (no reflection) - var order = JsonSerializer.Deserialize(inputStream); - - // Resolve services (no reflection) - var service = services.GetRequiredService(); - - // Execute handler - var result = service.Process(order); - - // Serialize response - return JsonSerializer.Serialize(result); -} -``` - -!!! info "Transparent Optimization" - You don't need to think about source generation—it just works. Write clean, simple handler code and let the framework optimize it. - -## Putting It All Together - -Here's a complete example demonstrating all core concepts: - -```csharp -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using AwsLambda.Host.Builder; -using AwsLambda.Host.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -// === SETUP === -var builder = LambdaApplication.CreateBuilder(); - -// Register services with appropriate lifetimes -builder.Services.AddSingleton(); // Shared across invocations -builder.Services.AddScoped(); // Per invocation - -var lambda = builder.Build(); - -// === LIFECYCLE: OnInit (runs once) === -lambda.OnInit( - async (services, cancellationToken) => - { - Console.WriteLine("=== Initializing Lambda ==="); - var cache = services.GetRequiredService(); - await cache.WarmupAsync(cancellationToken); - Console.WriteLine("=== Initialization Complete ==="); - return true; - } -); - -// === MIDDLEWARE 1: Logging === -lambda.UseMiddleware( - async (context, next) => - { - Console.WriteLine($"[{DateTime.UtcNow}] Request started"); - await next(context); - Console.WriteLine($"[{DateTime.UtcNow}] Request completed"); - } -); - -// === MIDDLEWARE 2: Timing === -lambda.UseMiddleware( - async (context, next) => - { - var stopwatch = Stopwatch.StartNew(); - await next(context); - stopwatch.Stop(); - Console.WriteLine($"Duration: {stopwatch.ElapsedMilliseconds}ms"); - } -); + // ReSharper disable once StaticMemberInGenericType + private static readonly Type FeatureType = typeof(IResponseFeature); -// === MIDDLEWARE 3: Correlation ID === -lambda.UseMiddleware( - async (context, next) => + /// + public bool TryCreate(Type type, out object? feature) { - var correlationId = Guid.NewGuid().ToString(); - context.Items["CorrelationId"] = correlationId; - Console.WriteLine($"Correlation ID: {correlationId}"); - await next(context); - } -); - -// === HANDLER: Business Logic with DI === -lambda.MapHandler( - async ( - [Event] Order order, // Lambda event - IOrderRepository repository, // Scoped service - ICache cache, // Singleton service - ILambdaHostContext context, // Framework context - CancellationToken cancellationToken // Cancellation token - ) => - { - // Check cache first - if (cache.TryGet(order.Id, out var cached)) - { - Console.WriteLine($"Cache hit for order {order.Id}"); - return cached; - } - - // Process order - Console.WriteLine($"Processing order {order.Id}"); - var result = await repository.ProcessAsync(order, cancellationToken); - - // Update cache - cache.Set(order.Id, result); - - return result; - } -); + feature = type == FeatureType ? new DefaultResponseFeature(lambdaSerializer) : null; -// === LIFECYCLE: OnShutdown (runs once) === -lambda.OnShutdown( - async (services, cancellationToken) => - { - Console.WriteLine("=== Shutting Down Lambda ==="); - var cache = services.GetRequiredService(); - // Flush cache or perform cleanup - Console.WriteLine("=== Shutdown Complete ==="); + return feature is not null; } -); - -// === RUN === -await lambda.RunAsync(); - -// === MODELS === -public record Order(string Id, decimal Amount); - -public record OrderResult(string OrderId, bool Success); - -// === SERVICES === -public interface ICache -{ - Task WarmupAsync(CancellationToken ct); - bool TryGet(string key, out OrderResult value); - void Set(string key, OrderResult value); -} - -public interface IOrderRepository -{ - Task ProcessAsync(Order order, CancellationToken ct); } ``` -## Key Takeaways - -Now you understand the core concepts of aws-lambda-host: +Feature providers are just services. Register them like any other dependency: -1. **Lambda Lifecycle** - - OnInit runs once on cold start (setup) - - Invocation runs for each event (business logic) - - OnShutdown runs once before termination (cleanup) - -2. **Dependency Injection** - - Singleton: Created once, shared across invocations (must be thread-safe) - - Scoped: Created per invocation, isolated and disposed after - -3. **Middleware Pipeline** - - Executes before and after handlers - - Perfect for cross-cutting concerns - - Composes into a nested pipeline +```csharp title="Program.cs" linenums="1" +builder.Services.AddSingleton(); +``` -4. **Handler Registration** - - Use `[Event]` to mark the Lambda event parameter - - Inject services directly into handlers - - Framework resolves dependencies automatically +## Organizing `Program.cs` -5. **Source Generation** - - Compile-time code generation - - Zero runtime reflection - - AOT compilation ready - - Compile-time validation +Keep supporting types—record definitions, services, and options classes—at the end of `Program.cs` after `await lambda.RunAsync();`. This keeps cold-start logic easy to follow and matches the source generator’s expectations when it scans the file for handler registrations. -### Explore Further +## Where to Go Next -- **[Middleware Guide](/guides/middleware.md)** – Advanced middleware patterns -- **[Dependency Injection Guide](/guides/dependency-injection.md)** – DI best practices -- **[Lifecycle Management](/guides/lifecycle-management.md)** – Deep dive into lifecycle hooks -- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns -- **[Source Generators](/advanced/source-generators.md)** – How source generation works internally +- **[Lifecycle Management](../guides/lifecycle-management.md)** – Deep dive into OnInit/OnShutdown configuration and cancellation. +- **[Dependency Injection](../guides/dependency-injection.md)** – Learn about scoped lifetimes, keyed services, and context injection in more detail. +- **[Middleware](../guides/middleware.md)** – Compose reusable pipeline components and feature access patterns. +- **[Handler Registration](../guides/handler-registration.md)** – Understand how source generation enforces handler signatures. +- **[Configuration](../guides/configuration.md)** – Bind `LambdaHostOptions` and application settings from `appsettings.*` or environment variables. diff --git a/mkdocs.yml b/mkdocs.yml index b97c025d..ad71b368 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,13 +99,13 @@ nav: - Configuration: guides/configuration.md - Error Handling: guides/error-handling.md - Testing: guides/testing.md - - Examples: + - Examples (Coming Soon): - examples/index.md - Features: - features/index.md - Envelopes: features/envelopes.md - OpenTelemetry: features/open_telemetry.md - - Advanced: + - Advanced (Coming Soon): - advanced/index.md plugins: From f6a7bf401c2c9f7fa5346d2e1c1ab7aa4e68e9ab Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 14:54:05 -0500 Subject: [PATCH 55/65] feat(docs): update configuration guide with enhanced examples and best practices - Improved `LambdaHostOptions` section with detailed examples and updated table formats. - Added JSON and Bash code blocks to illustrate both file-based and environment variable configurations. - Enhanced examples for `ShutdownDuration`, `ClearLambdaOutputFormatting`, and custom serializer options. - Condensed `Options Pattern` section with clear guidance on environment-specific settings and validation. - Updated --- docs/guides/configuration.md | 776 ++++++++--------------------------- 1 file changed, 178 insertions(+), 598 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index a51cf07c..b7975ce8 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -1,30 +1,47 @@ # Configuration -Proper configuration is essential for building robust Lambda functions. aws-lambda-host provides -flexible configuration through `LambdaHostOptions` for framework behavior, plus standard ASP.NET -Core configuration patterns for application settings. +`AwsLambda.Host` embraces the same configuration primitives as the rest of .NET: `IConfiguration` aggregates +settings from files and environment variables, and options bind those settings into strongly typed +objects. On top of that, `LambdaHostOptions` control the Lambda-specific runtime behavior (timeouts, +shutdown windows, serializer choices, etc.). This guide covers both layers. -## Introduction +## How Configuration Is Built -Configuration in aws-lambda-host falls into two categories: +`LambdaApplication.CreateBuilder()` wires up the standard .NET defaults: -1. **Framework Configuration** – `LambdaHostOptions` controls Lambda lifecycle, timeouts, and - serialization -2. **Application Configuration** – `appsettings.json`, environment variables, and the options - pattern for your business logic +1. `appsettings.json` (optional) in the application root. +2. `appsettings.{Environment}.json` (optional) based on `DOTNET_ENVIRONMENT`/`ASPNETCORE_ENVIRONMENT`. +3. User secrets (Development only). +4. Environment variables (`AWS_`, `DOTNET_`, and general variables). -Both integrate seamlessly with `Microsoft.Extensions.Configuration` and dependency injection. +The builder also binds the `AwsLambdaHost` section (from JSON or environment variables) into +`LambdaHostOptions` so framework settings can live next to your app configuration. -## LambdaHostOptions +```json title="appsettings.json" +{ + "AwsLambdaHost": { + "InitTimeout": "00:00:10", + "InvocationCancellationBuffer": "00:00:05", + "ShutdownDuration": "00:00:00.5000000", + "ShutdownDurationBuffer": "00:00:00.0500000", + "ClearLambdaOutputFormatting": true + } +} +``` + +Alternatively, set environment variables using the `AwsLambdaHost__{Option}` naming convention: -`LambdaHostOptions` controls core framework behavior. +```bash +AwsLambdaHost__InvocationCancellationBuffer=00:00:05 +AwsLambdaHost__ClearLambdaOutputFormatting=true +``` -### Configuration Method +## LambdaHostOptions Reference -```csharp title="Program.cs" -using AwsLambda.Host; -using Microsoft.Extensions.DependencyInjection; +Use `builder.Services.ConfigureLambdaHostOptions` to override framework behavior at runtime or during tests. +Configuration settings and imperative code are additive—the explicit delegate runs after configuration binding. +```csharp title="Program.cs" linenums="1" var builder = LambdaApplication.CreateBuilder(); builder.Services.ConfigureLambdaHostOptions(options => @@ -35,27 +52,24 @@ builder.Services.ConfigureLambdaHostOptions(options => options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); options.ClearLambdaOutputFormatting = true; }); - -var lambda = builder.Build(); ``` -### Configuration Options Reference - -| Option | Type | Default | Description | -|--------------------------------|--------------------------|-----------|------------------------------------------------------------| -| `InitTimeout` | `TimeSpan` | 5 seconds | Maximum time for OnInit phase before cancellation | -| `InvocationCancellationBuffer` | `TimeSpan` | 3 seconds | Buffer before Lambda timeout to trigger cancellation token | -| `ShutdownDuration` | `TimeSpan` | 500ms | Time between SIGTERM and SIGKILL signals | -| `ShutdownDurationBuffer` | `TimeSpan` | 50ms | Safety buffer subtracted from ShutdownDuration | -| `ClearLambdaOutputFormatting` | `bool` | `false` | Remove Lambda runtime log formatting | -| `BootstrapHttpClient` | `HttpClient?` | `null` | Custom HTTP client for Lambda bootstrap | -| `BootstrapOptions` | `LambdaBootstrapOptions` | new() | Lambda runtime bootstrap configuration | +| Option | Type | Default | Description | +|--------------------------------|--------------------------|------------------------------------|-----------------------------------------------------------------------------| +| `InitTimeout` | `TimeSpan` | 5 seconds | Maximum time all `OnInit` handlers collectively have before cancellation. | +| `InvocationCancellationBuffer` | `TimeSpan` | 3 seconds | Buffer subtracted from remaining time before the invocation token fires. | +| `ShutdownDuration` | `TimeSpan` | `ShutdownDuration.ExternalExtensions` (500 ms) | Expected window between SIGTERM and SIGKILL. | +| `ShutdownDurationBuffer` | `TimeSpan` | 50 ms | Safety margin deducted from `ShutdownDuration` to guarantee cleanup. | +| `ClearLambdaOutputFormatting` | `bool` | `false` | Automatically register the built-in OnInit handler that clears console formatting. | +| `BootstrapHttpClient` | `HttpClient?` | `null` | Custom client for the Lambda bootstrap runtime API. | +| `BootstrapOptions` | `LambdaBootstrapOptions` | `new()` | Low-level runtime bootstrap configuration (timeouts, heartbeats, etc.). | -### InitTimeout +### `InitTimeout` -Controls how long `OnInit` handlers can run before cancellation. +Controls how long `OnInit` handlers can run before the host cancels them. Keep cold starts fast by +splitting heavy work into multiple handlers—they execute in parallel but still share this budget. -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" builder.Services.ConfigureLambdaHostOptions(options => { options.InitTimeout = TimeSpan.FromSeconds(10); @@ -63,135 +77,105 @@ builder.Services.ConfigureLambdaHostOptions(options => lambda.OnInit(async (ICache cache, CancellationToken ct) => { - // 'ct' fires after 10 seconds - await cache.WarmUpAsync(ct); + await cache.WarmUpAsync(ct); // ct fires after 10 seconds return true; }); ``` -**When to adjust:** - -- **Increase** if OnInit performs expensive operations (loading ML models, warming large caches) -- **Decrease** for faster feedback during initialization failures - -**Default:** 5 seconds +### `InvocationCancellationBuffer` -### InvocationCancellationBuffer +Determines when the invocation-scoped `CancellationToken` fires relative to the Lambda timeout. A +buffer gives you time to short-circuit work and flush telemetry before AWS abruptly stops the process. -Controls when the invocation `CancellationToken` fires relative to Lambda's hard timeout. - -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" builder.Services.ConfigureLambdaHostOptions(options => { - // Fire cancellation 5 seconds before Lambda times out + // Fire cancellation 5 seconds before the Lambda timeout expires options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); }); lambda.MapHandler(async ([Event] Order order, IOrderService service, CancellationToken ct) => -{ - // If Lambda timeout is 30s, 'ct' fires after 25s - return await service.ProcessAsync(order, ct); -}); + await service.ProcessAsync(order, ct)); ``` -**Why it's needed:** - -- Allows graceful cancellation before Lambda's hard timeout -- Gives time for cleanup and error responses -- Prevents incomplete transactions +### `ShutdownDuration` and `ShutdownDurationBuffer` -**When to adjust:** +Lambda signals shutdown with SIGTERM and later SIGKILL. Configure how long you expect to have between +those signals and how much buffer `AwsLambda.Host` should reserve before cancelling `OnShutdown` +handlers. -- **Increase** for operations requiring significant cleanup time -- **Decrease** to maximize processing time per invocation - -**Default:** 3 seconds - -### ShutdownDuration - -Time between SIGTERM (shutdown signal) and SIGKILL (forced termination). - -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" builder.Services.ConfigureLambdaHostOptions(options => { - // Use predefined constant for external extensions - options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500ms + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500 ms + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(75); // cancel after 425 ms }); -``` - -**Available Constants:** - -```csharp -// No extension time (0ms) -options.ShutdownDuration = ShutdownDuration.NoExtensions; - -// Internal extensions only (300ms) -options.ShutdownDuration = ShutdownDuration.InternalExtensions; -// External extensions (500ms) - DEFAULT -options.ShutdownDuration = ShutdownDuration.ExternalExtensions; - -// Custom duration -options.ShutdownDuration = TimeSpan.FromSeconds(2); +lambda.OnShutdown(async (ITelemetry telemetry, CancellationToken ct) => + await telemetry.FlushAsync(ct)); ``` -**When to adjust:** - -- **Use NoExtensions** (0ms) if no Lambda extensions installed -- **Use InternalExtensions** (300ms) for internal-only extensions -- **Use ExternalExtensions** (500ms) if using external extensions (default) -- **Increase** if shutdown tasks require more time +Choose from the provided constants when possible: -**Default:** `ShutdownDuration.ExternalExtensions` (500ms) +- `ShutdownDuration.NoExtensions` – No extensions installed (0 ms window). +- `ShutdownDuration.InternalExtensions` – Only internal extensions (300 ms). +- `ShutdownDuration.ExternalExtensions` – External extension installed (500 ms, default). +- Any custom `TimeSpan` when your environment requires more/less time. -### ShutdownDurationBuffer +### `ClearLambdaOutputFormatting` -Safety buffer subtracted from `ShutdownDuration` to ensure cleanup completes before SIGKILL. +The .NET Lambda runtime captures stdout/stderr and wraps each line with its own structured record. +When you rely on structured loggers (Serilog, MEL JSON) or run locally, disable that extra formatting. +`AwsLambda.Host` registers the built-in OnInit handler automatically when this option is `true`. -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" builder.Services.ConfigureLambdaHostOptions(options => { - options.ShutdownDuration = TimeSpan.FromSeconds(1); - options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); - - // Actual shutdown timeout: 1000ms - 100ms = 900ms -}); - -lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => -{ - // 'ct' fires after 900ms - await metrics.FlushAsync(ct); + options.ClearLambdaOutputFormatting = true; }); ``` -**Default:** 50ms +Prefer configuration files? Set `"AwsLambdaHost": { "ClearLambdaOutputFormatting": true }` or the +`AwsLambdaHost__ClearLambdaOutputFormatting` environment variable. -### ClearLambdaOutputFormatting +### `BootstrapHttpClient` -Removes Lambda runtime's custom log formatting, useful for structured logging frameworks. +Override the HTTP client the Lambda bootstrap uses to poll the runtime API. Most projects never touch +this, but it is handy when you need custom TLS settings or want to share a pre-configured handler for +proxy scenarios. -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" builder.Services.ConfigureLambdaHostOptions(options => { - options.ClearLambdaOutputFormatting = true; + options.BootstrapHttpClient = new HttpClient(new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(5), + Proxy = WebRequest.DefaultWebProxy, + }); }); ``` -**Why use it:** +### `BootstrapOptions` -- Lambda runtime adds formatting that interferes with JSON logging -- Structured logging (Serilog, NLog) produces malformed JSON without this -- CloudWatch Logs Insights parses logs correctly +`LambdaBootstrapOptions` mirror the parameters in `Amazon.Lambda.RuntimeSupport.Bootstrap`. Expose +these only when you need to tune lower-level runtime behavior such as the number of inflight +invocations or how long the bootstrap waits when polling. -**Default:** `false` +```csharp title="Program.cs" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.BootstrapOptions = new LambdaBootstrapOptions + { + Client = LambdaBootstrapOptions.Client.HttpClient, + TelemetryFlushLatency = TimeSpan.FromMilliseconds(25) + }; +}); +``` ## Application Configuration -Use standard ASP.NET Core configuration for application settings. - -### appsettings.json - -Create `appsettings.json` in your project root: +Everything outside `LambdaHostOptions` works the same way it does in .NET. Use +`appsettings.json`, bind to options classes, and inject `IOptions` into services. ```json title="appsettings.json" { @@ -209,185 +193,78 @@ Create `appsettings.json` in your project root: "Database": { "ConnectionString": "Server=localhost;Database=orders", "CommandTimeout": 30 - }, - "ExternalApi": { - "BaseUrl": "https://api.example.com", - "ApiKey": "" - // Set via environment variable } } ``` -**Important:** Include in `.csproj`: - -```xml +Include configuration files in your deployment output so Lambda can read them at runtime: +```xml title="MyLambda.csproj" PreserveNewest + + PreserveNewest + ``` ### Options Pattern -Define strongly-typed options classes: - -```csharp title="Configuration/OrderProcessingOptions.cs" -namespace MyLambda.Configuration; +Create strongly typed classes and bind them inside `Program.cs`: -public class OrderProcessingOptions -{ - public int MaxRetries { get; init; } - public int TimeoutSeconds { get; init; } - public bool EnableCaching { get; init; } -} -``` - -Bind configuration sections to options: - -```csharp title="Program.cs" -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using MyLambda.Configuration; - -var builder = LambdaApplication.CreateBuilder(); - -// Bind configuration section to options class +```csharp title="Program.cs" linenums="1" builder.Services.Configure( - builder.Configuration.GetSection("OrderProcessing") -); + builder.Configuration.GetSection("OrderProcessing")); builder.Services.AddScoped(); - -var lambda = builder.Build(); ``` -Inject options into services: - -```csharp title="Services/OrderService.cs" -using Microsoft.Extensions.Options; -using MyLambda.Configuration; +Inside services, inject `IOptions` (or `IOptionsSnapshot`/`IOptionsMonitor` when you truly +need scoped/snapshot behavior). Lambda deployments rarely change configuration at runtime, so +`IOptions`—captured once during cold start—is the most predictable choice. +```csharp title="OrderService.cs" linenums="1" public class OrderService : IOrderService { private readonly OrderProcessingOptions _options; - private readonly IOrderRepository _repository; - public OrderService( - IOptions options, - IOrderRepository repository) - { + public OrderService(IOptions options) => _options = options.Value; - _repository = repository; - } - public async Task ProcessAsync(Order order) - { - if (_options.EnableCaching) - { - // Check cache - } - - for (int retry = 0; retry < _options.MaxRetries; retry++) - { - try - { - return await _repository.SaveAsync(order); - } - catch (Exception) when (retry < _options.MaxRetries - 1) - { - await Task.Delay(TimeSpan.FromSeconds(1)); - } - } - - throw new Exception("Max retries exceeded"); - } + public Task ProcessAsync(Order order, CancellationToken ct) => + _options.EnableCaching + ? ProcessWithCacheAsync(order, ct) + : ProcessWithoutCacheAsync(order, ct); } ``` -### IOptions vs IOptionsSnapshot vs IOptionsMonitor +| Interface | Lifetime | Reloads | Lambda Guidance | +|-----------------------|-----------|----------------|--------------------------------------------------------------------| +| `IOptions` | Singleton | Never | ✅ Recommended—configuration ships with the deployment package. | +| `IOptionsSnapshot` | Scoped | Per invocation | Use only if you vary config between invocations (rare). | +| `IOptionsMonitor` | Singleton | On change | Rarely useful unless you reload from remote providers at runtime. | -| Interface | Lifetime | Reloads | Use Case | -|-----------------------|-----------|----------------|---------------------------------------------------------------------| -| `IOptions` | Singleton | Never | **Recommended for Lambda** – Config doesn't change during execution | -| `IOptionsSnapshot` | Scoped | Per invocation | Use if config can change between invocations | -| `IOptionsMonitor` | Singleton | On change | Rarely needed in Lambda | +### Environment-Specific Files -**Recommendation:** Use `IOptions` for Lambda functions. - -```csharp -// GOOD: IOptions for Lambda -public OrderService(IOptions options) -{ - _options = options.Value; -} -``` - -## Environment-Specific Configuration - -Use multiple configuration files for different environments. - -### File Structure +Ship overrides per environment by naming convention: ``` MyLambda/ -├── appsettings.json # Base configuration -├── appsettings.Development.json # Development overrides -└── appsettings.Production.json # Production overrides -``` - -### Configuration Loading - -```csharp title="Program.cs" -var builder = LambdaApplication.CreateBuilder(); - -// Configuration loaded automatically in this order: -// 1. appsettings.json (base) -// 2. appsettings.{Environment}.json (environment-specific) -// 3. Environment variables (highest priority) - -// Add additional configuration sources if needed -builder.Configuration.AddEnvironmentVariables(); -``` - -### Environment-Specific Settings - -```json title="appsettings.Development.json" -{ - "OrderProcessing": { - "EnableCaching": false - }, - "Database": { - "ConnectionString": "Server=localhost;Database=orders_dev" - } -} -``` - -```json title="appsettings.Production.json" -{ - "OrderProcessing": { - "EnableCaching": true - }, - "Database": { - "ConnectionString": "" - // Set via environment variable - } -} +├── appsettings.json +├── appsettings.Development.json +└── appsettings.Production.json ``` -## Environment Variables - -Environment variables override configuration files. +The builder automatically loads `appsettings.{Environment}.json` when `DOTNET_ENVIRONMENT` is set. +You can add more sources manually via `builder.Configuration.AddJsonFile(...)` or +`builder.Configuration.AddEnvironmentVariables()`. -### Setting Environment Variables +### Environment Variables -In Lambda, set environment variables through: - -- AWS Console -- AWS SAM template -- AWS CDK -- Terraform +Environment variables override configuration files. Configure them via the AWS Console, SAM/CDK +templates, Terraform, or GitHub Actions. ```yaml title="template.yaml" linenums="1" Resources: @@ -396,395 +273,98 @@ Resources: Properties: Environment: Variables: - DATABASE_CONNECTION_STRING: !Ref DatabaseConnectionString + DATABASE__CONNECTIONSTRING: !Ref DatabaseConnectionString API_KEY: !Ref ApiKey - ASPNETCORE_ENVIRONMENT: Production + AwsLambdaHost__ClearLambdaOutputFormatting: true ``` -### Accessing Environment Variables - -#### Direct Access +Prefer the double underscore `__` separator when targeting hierarchical keys. -```csharp title="Program.cs" -var apiKey = Environment.GetEnvironmentVariable("API_KEY"); -``` - -#### Through Configuration +Access the values directly or through bound options: -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" var apiKey = builder.Configuration["API_KEY"]; -``` - -#### Binding to Options - -```csharp title="Program.cs" -public class ExternalApiOptions -{ - public string BaseUrl { get; init; } = ""; - public string ApiKey { get; init; } = ""; -} builder.Services.Configure(options => { - options.BaseUrl = builder.Configuration["ExternalApi:BaseUrl"] ?? ""; - options.ApiKey = builder.Configuration["API_KEY"] ?? ""; + options.BaseUrl = builder.Configuration["ExternalApi:BaseUrl"] ?? string.Empty; + options.ApiKey = builder.Configuration["API_KEY"] ?? string.Empty; }); ``` -### Environment Variable Naming - -Configuration binding supports hierarchical keys with `:` or `__`: - -```bash -# Both work -ExternalApi:BaseUrl=https://api.example.com -ExternalApi__BaseUrl=https://api.example.com -``` - -**Prefer `__`** for environment variables (`:` not supported in all shells). - ## JSON Serialization Configuration -Configure JSON serialization for Lambda events and responses. - -### Default Serialization - -The framework uses `System.Text.Json` with sensible defaults: +`AwsLambda.Host` defaults to `System.Text.Json` with camelCase naming, case-insensitive reads, and +null-value ignoring. Because the Lambda serializer is an `ILambdaSerializer` singleton, you configure +it by registering the serializer instance you want (either the built-in +`DefaultLambdaJsonSerializer` with custom `JsonSerializerOptions` or your own implementation). -```csharp -// Default configuration (no action needed) -// - Camel case property names -// - Case-insensitive deserialization -// - Ignores null values -``` - -### Custom JsonSerializerContext (AOT) +### Native AOT / Source-Generated Serialization -For Native AOT compilation, define a JSON serializer context: - -```csharp title="Program.cs" -using System.Text.Json.Serialization; +Native AOT deployments require a `JsonSerializerContext` that describes every type the handler emits +or consumes. +```csharp title="MySerializerContext.cs" linenums="1" [JsonSerializable(typeof(Request))] [JsonSerializable(typeof(Response))] -[JsonSerializable(typeof(Order))] public partial class MySerializerContext : JsonSerializerContext; -var builder = LambdaApplication.CreateBuilder(); - -// Register custom serializer context builder.Services.AddLambdaSerializerWithContext(); - -var lambda = builder.Build(); ``` -**Benefits:** - -- Required for Native AOT -- Compile-time serialization metadata -- Better trimming -- Faster performance - -### Custom JsonSerializerOptions +### Custom Serializer Options -Customize JSON serialization options: - -```csharp title="Program.cs" -using System.Text.Json; - -builder.Services.Configure(options => -{ - options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - options.WriteIndented = false; - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; -}); -``` - -## Secrets Management - -Never hardcode secrets in configuration files. - -### AWS Secrets Manager - -```csharp title="Program.cs" -using Amazon; -using Amazon.Extensions.Configuration.SystemsManager; - -var builder = LambdaApplication.CreateBuilder(); - -// Add AWS Secrets Manager as configuration source -builder.Configuration.AddSecretsManager( - region: RegionEndpoint.USEast1, - configurator: options => +```csharp title="Program.cs" linenums="1" +builder.Services.AddSingleton(_ => + new DefaultLambdaJsonSerializer(options => { - options.SecretFilter = entry => entry.Name.StartsWith("MyLambda/"); - options.PollingInterval = TimeSpan.FromMinutes(5); - } + options.PropertyNamingPolicy = null; // keep property names as-is + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + }) ); ``` -**Install NuGet package:** - -```bash -dotnet add package Amazon.Extensions.Configuration.SystemsManager -``` - -### Environment Variables for Secrets - -```csharp title="Program.cs" -public class DatabaseOptions -{ - public string ConnectionString { get; init; } = ""; -} - -builder.Services.Configure(options => -{ - // Set via Lambda environment variable - options.ConnectionString = builder.Configuration["DATABASE_CONNECTION_STRING"] ?? ""; -}); -``` - -**In template.yaml:** - -```yaml -Environment: - Variables: - DATABASE_CONNECTION_STRING: !Sub '{{resolve:secretsmanager:${DatabaseSecret}:SecretString:connectionString}}' -``` +Registering the serializer replaces the default `ILambdaSerializer`. Use this pattern when you need to +tweak naming policies, converters, or reference handling without adopting a source-generated context. ## Configuration Validation -Validate configuration at startup to catch errors early. - -### Data Annotations - -```csharp title="Configuration/OrderProcessingOptions.cs" -using System.ComponentModel.DataAnnotations; - -public class OrderProcessingOptions -{ - [Range(1, 10)] - public int MaxRetries { get; init; } - - [Range(1, 300)] - public int TimeoutSeconds { get; init; } - - public bool EnableCaching { get; init; } -} -``` +Validate bound options at startup so bad configuration never reaches production. -### Validation on Startup - -```csharp title="Program.cs" +```csharp title="Program.cs" linenums="1" builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("OrderProcessing")) .ValidateDataAnnotations() .ValidateOnStart(); ``` -**Validation fails on startup** if configuration is invalid. - -### Custom Validation - -```csharp title="Configuration/OrderProcessingOptions.cs" -public class OrderProcessingOptions : IValidatableObject -{ - public int MaxRetries { get; init; } - public int TimeoutSeconds { get; init; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (MaxRetries < 1) - { - yield return new ValidationResult( - "MaxRetries must be at least 1", - new[] { nameof(MaxRetries) } - ); - } - - if (TimeoutSeconds > 300) - { - yield return new ValidationResult( - "TimeoutSeconds cannot exceed 300", - new[] { nameof(TimeoutSeconds) } - ); - } - } -} -``` +For more complex rules implement `IValidatableObject` or add a custom validator. ## Best Practices -### ✅ Do: Use Strongly-Typed Options - -```csharp -// GOOD: Strongly-typed options -builder.Services.Configure( - builder.Configuration.GetSection("OrderProcessing") -); -``` - -### ❌ Don't: Use Magic Strings - -```csharp -// BAD: Magic strings everywhere -var maxRetries = int.Parse(builder.Configuration["OrderProcessing:MaxRetries"]); -var timeout = int.Parse(builder.Configuration["OrderProcessing:TimeoutSeconds"]); -``` - -### ✅ Do: Validate Configuration at Startup - -```csharp -// GOOD: Validate on startup -builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection("OrderProcessing")) - .ValidateDataAnnotations() - .ValidateOnStart(); -``` - -### ✅ Do: Use Environment-Specific Files - -```csharp -// GOOD: Environment-specific configuration -// appsettings.json -// appsettings.Development.json -// appsettings.Production.json -``` - -### ❌ Don't: Hardcode Configuration Values - -```csharp -// BAD: Hardcoded values -public class OrderService -{ - private const int MaxRetries = 3; - private const string ApiUrl = "https://api.example.com"; -} -``` - -### ✅ Do: Use Environment Variables for Secrets - -```csharp -// GOOD: Secrets via environment variables -var apiKey = builder.Configuration["API_KEY"]; -``` - -### ❌ Don't: Commit Secrets to Source Control - -```json -// BAD: API key in appsettings.json -{ - "ExternalApi": { - "ApiKey": "sk_live_51234567890abcdef" - // DON'T! - } -} -``` - -### ✅ Do: Include appsettings.json in Deployment - -```xml - - - - PreserveNewest - - -``` - -### ✅ Do: Use IOptions for Lambda - -```csharp -// GOOD: IOptions for Lambda (config doesn't reload) -public OrderService(IOptions options) -{ - _options = options.Value; -} -``` - -### ❌ Don't: Use IOptionsMonitor in Lambda - -```csharp -// BAD: IOptionsMonitor rarely needed in Lambda -public OrderService(IOptionsMonitor options) -{ - _options = options.CurrentValue; // Unnecessary overhead -} -``` +- **Prefer `IOptions`** – Configuration rarely changes per invocation; capture it during cold start. +- **Validate on startup** – `ValidateOnStart()` catches missing sections before Lambda accepts traffic. +- **Copy `appsettings.*` to the output** – Without it, Lambda cannot load the files. +- **Use environment variables for secrets** – Combine SAM/CDK parameters with Secrets Manager references. +- **Stick to `AwsLambdaHost` section for framework knobs** – Keeps host settings discoverable and + separate from business configuration. +- **Clear Lambda output formatting when you own logging** – Avoid double-wrapping JSON payloads. ## Troubleshooting -### Configuration Section Not Found - -**Problem:** `options.Value` is null or has default values. - -**Solution:** Verify configuration section exists and binding is correct: - -```csharp -// Check configuration exists -var section = builder.Configuration.GetSection("OrderProcessing"); -if (!section.Exists()) -{ - throw new InvalidOperationException("OrderProcessing configuration section not found"); -} - -// Bind with validation -builder.Services.AddOptions() - .Bind(section) - .ValidateDataAnnotations() - .ValidateOnStart(); -``` - -### appsettings.json Not Found - -**Problem:** Configuration file not deployed with Lambda. - -**Solution:** Ensure `appsettings.json` is copied to output: - -```xml +**Configuration section not found** – Call `Exists()` on the section and ensure the file is copied to +`bin/Release/net8.0/publish`. - - - PreserveNewest - - -``` +**`appsettings.json` missing in production** – Confirm `PreserveNewest` is set for each config file. -### Environment Variable Not Loaded +**Environment variable not loaded** – Remember to add `builder.Configuration.AddEnvironmentVariables()` if you call `CreateBuilder(new LambdaApplicationOptions { DisableDefaults = true })`. -**Problem:** Environment variable set but not accessible. - -**Solution:** Ensure environment variables are loaded: - -```csharp -builder.Configuration.AddEnvironmentVariables(); -``` - -**Or:** Check Lambda environment variable configuration in AWS Console/template. - -## Key Takeaways - -1. **LambdaHostOptions** – Configure framework behavior (timeouts, shutdown, serialization) -2. **appsettings.json** – Store application configuration -3. **Options Pattern** – Use `IOptions` for strongly-typed configuration -4. **Environment Variables** – Override configuration, store secrets -5. **Validation** – Validate configuration at startup -6. **JSON Serialization** – Customize with `JsonSerializerOptions` or `JsonSerializerContext` -7. **Secrets** – Use environment variables or AWS Secrets Manager, never commit secrets -8. **IOptions** – Prefer over `IOptionsSnapshot` or `IOptionsMonitor` for Lambda +**LambdaHostOptions ignored** – Verify the JSON is under `"AwsLambdaHost"` or that the environment +variable uses `AwsLambdaHost__OptionName`. ## Next Steps -Now that you understand configuration, explore related topics: - -- **[Lifecycle Management](/guides/lifecycle-management.md)** – Use configuration in OnInit and - OnShutdown -- **[Dependency Injection](/guides/dependency-injection.md)** – Inject configured options into - services -- **[Error Handling](/guides/error-handling.md)** – Configure timeout buffers -- **[Deployment](/guides/deployment.md)** – Set environment variables in deployment templates -- **[Testing](/guides/testing.md)** – Test services with different configurations - ---- - -Congratulations! You now understand how to configure aws-lambda-host and your Lambda functions. +- **[Lifecycle Management](lifecycle-management.md)** – Use `LambdaHostOptions` to fine-tune OnInit/OnShutdown. +- **[Dependency Injection](dependency-injection.md)** – Inject configured options and keyed services safely. +- **[Error Handling](error-handling.md)** – Tie timeout settings to retry logic. +- **[Testing](testing.md)** – Override configuration in unit tests with in-memory providers. From 0746b0d2dc7a251c7163964043a51e8532bfc00e Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 15:17:02 -0500 Subject: [PATCH 56/65] feat(docs): update references to `AwsLambda.Host` for consistency - Replaced all mentions of `aws-lambda-host` with `AwsLambda.Host` across documentation for consistency. - Updated examples, headings, and explanations to align with the standardized naming convention. - Improved clarity by ensuring formatting consistency in code snippets, tables, and lists. - Enhanced readability in `Error Handling`, `Getting Started`, and `Core Concepts` guides. --- docs/advanced/index.md | 2 +- docs/getting-started/core-concepts.md | 12 +- docs/getting-started/first-lambda.md | 6 +- docs/getting-started/index.md | 4 +- docs/getting-started/installation.md | 6 +- docs/guides/dependency-injection.md | 2 +- docs/guides/error-handling.md | 969 ++------------------------ docs/guides/handler-registration.md | 2 +- docs/guides/index.md | 4 +- docs/guides/middleware.md | 2 +- docs/index.md | 2 +- 11 files changed, 87 insertions(+), 924 deletions(-) diff --git a/docs/advanced/index.md b/docs/advanced/index.md index 8df0e409..9ec02e9e 100644 --- a/docs/advanced/index.md +++ b/docs/advanced/index.md @@ -1,5 +1,5 @@ # Advanced Topics -> Coming soon: guidance on Native AOT, source generator internals, and performance tuning for aws-lambda-host. +> Coming soon: guidance on Native AOT, source generator internals, and performance tuning for `AwsLambda.Host`. We're working on detailed content for advanced scenarios. In the meantime, follow the repository's CHANGELOG and GitHub discussions for early tips. diff --git a/docs/getting-started/core-concepts.md b/docs/getting-started/core-concepts.md index c25b4dce..ea3b47e9 100644 --- a/docs/getting-started/core-concepts.md +++ b/docs/getting-started/core-concepts.md @@ -1,10 +1,10 @@ # Core Concepts -This guide explains how aws-lambda-host stitches together the Lambda lifecycle, dependency injection, middleware, and source generation. Knowing these building blocks makes it easier to reason about performance, testing, and extensibility as your functions grow. +This guide explains how `AwsLambda.Host` stitches together the Lambda lifecycle, dependency injection, middleware, and source generation. Knowing these building blocks makes it easier to reason about performance, testing, and extensibility as your functions grow. ## The Lambda Lifecycle -An execution environment progresses through distinct stages. aws-lambda-host exposes each one so you can opt into work at the right time. +An execution environment progresses through distinct stages. `AwsLambda.Host` exposes each one so you can opt into work at the right time. ``` Cold Start → OnInit → Invocation 1..N → OnShutdown → Termination @@ -43,13 +43,13 @@ lambda.OnInit(async ( ### Invocation Pipeline -Every event runs through the middleware pipeline and handler you registered with `MapHandler`. aws-lambda-host creates a scoped service provider, sets up features, and links the cancellation token to the Lambda timeout. +Every event runs through the middleware pipeline and handler you registered with `MapHandler`. `AwsLambda.Host` creates a scoped service provider, sets up features, and links the cancellation token to the Lambda timeout. **Invocation steps:** 1. Lambda runtime delivers the JSON payload. 2. The generated handler deserializes it (if you marked a parameter with `[Event]`). -3. aws-lambda-host creates a scoped `IServiceProvider` for the invocation. +3. `AwsLambda.Host` creates a scoped `IServiceProvider` for the invocation. 4. Middleware executes in the order it was registered. 5. Your handler runs and can resolve services/contexts from DI. 6. The response is captured through `IResponseFeature` and serialized via the configured `ILambdaSerializer`. @@ -104,7 +104,7 @@ graph LR ## Dependency Injection Fundamentals -aws-lambda-host uses the standard `Microsoft.Extensions.DependencyInjection` container. Registrations happen before `builder.Build()`, and the framework aligns service lifetimes with the Lambda lifecycle. +`AwsLambda.Host` uses the standard `Microsoft.Extensions.DependencyInjection` container. Registrations happen before `builder.Build()`, and the framework aligns service lifetimes with the Lambda lifecycle. | Lifetime | Created | Disposed | Use for | |-----------|--------------------------------------|-------------------------------------------|---------------------------------------------| @@ -158,7 +158,7 @@ Handlers and lifecycle hooks can request multiple parameter types simultaneously ## Middleware Pipeline -Middleware composes the invocation pipeline. Register delegates before `MapHandler`; aws-lambda-host wraps them around the handler in the order you specify. +Middleware composes the invocation pipeline. Register delegates before `MapHandler`; `AwsLambda.Host` wraps them around the handler in the order you specify. ```csharp title="Program.cs" linenums="1" lambda.UseMiddleware(async (context, next) => diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md index efb2a572..988f03d7 100644 --- a/docs/getting-started/first-lambda.md +++ b/docs/getting-started/first-lambda.md @@ -1,6 +1,6 @@ # Your First Lambda Function -This tutorial walks you through building, testing, and deploying a complete Lambda function using aws-lambda-host. You'll create a greeting service that demonstrates dependency injection, middleware, and type-safe handlers. +This tutorial walks you through building, testing, and deploying a complete Lambda function using `AwsLambda.Host`. You'll create a greeting service that demonstrates dependency injection, middleware, and type-safe handlers. ## What We'll Build @@ -17,7 +17,7 @@ A greeting service Lambda function with: Before starting, ensure you've completed the [Installation](installation.md) guide and have: -- aws-lambda-host NuGet package installed +- `AwsLambda.Host` NuGet package installed - Project file properly configured - .NET 8 SDK installed @@ -594,7 +594,7 @@ sequenceDiagram Congratulations! You've built and deployed a complete Lambda function. You now understand: -- ✅ How to structure Lambda functions with aws-lambda-host +- ✅ How to structure Lambda functions with `AwsLambda.Host` - ✅ Defining strongly-typed request/response models - ✅ Creating and registering services with dependency injection - ✅ Adding middleware for cross-cutting concerns diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index bba5f016..d1dbee47 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,8 +1,8 @@ # Getting Started -**aws-lambda-host** brings ASP.NET Core–style hosting, dependency injection, and middleware to AWS Lambda. Instead of wiring up serialization and context handling manually, you configure a Lambda-specific host that manages scopes, middleware, and strongly typed handlers at compile time. +**`AwsLambda.Host`** brings ASP.NET Core–style hosting, dependency injection, and middleware to AWS Lambda. Instead of wiring up serialization and context handling manually, you configure a Lambda-specific host that manages scopes, middleware, and strongly typed handlers at compile time. -### Why aws-lambda-host? +### Why `AwsLambda.Host`? - **Familiar patterns** – Builder APIs, DI, and middleware mirror ASP.NET Core. - **Source-generated handlers** – Avoid reflection while staying AOT ready. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 457aa97d..505c5dce 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,6 +1,6 @@ # Installation -This guide walks you through installing aws-lambda-host and configuring your project to build Lambda functions. +This guide walks you through installing `AwsLambda.Host` and configuring your project to build Lambda functions. ## System Requirements @@ -174,7 +174,7 @@ Build succeeded. ## Package Overview -The aws-lambda-host framework includes multiple packages for different use cases: +The `AwsLambda.Host` framework includes multiple packages for different use cases: ### Core Packages @@ -238,7 +238,7 @@ If you encounter issues not covered here: ## Next Steps -Now that you have aws-lambda-host installed and verified, you're ready to build your first Lambda function! +Now that you have `AwsLambda.Host` installed and verified, you're ready to build your first Lambda function! **→ Continue to [Your First Lambda](first-lambda.md)** to build a complete Lambda function step-by-step. diff --git a/docs/guides/dependency-injection.md b/docs/guides/dependency-injection.md index 68319f7c..952d9144 100644 --- a/docs/guides/dependency-injection.md +++ b/docs/guides/dependency-injection.md @@ -147,4 +147,4 @@ lambda.MapHandler(( - Use the options pattern for config and keyed services for multiple implementations. - For fundamentals, refer back to the [official DI docs](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection). -With these patterns, aws-lambda-host feels just like ASP.NET Core, but tuned for the Lambda lifecycle. +With these patterns, `AwsLambda.Host` feels just like ASP.NET Core, but tuned for the Lambda lifecycle. diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md index 854ebdaf..00ef16dc 100644 --- a/docs/guides/error-handling.md +++ b/docs/guides/error-handling.md @@ -1,100 +1,30 @@ # Error Handling -Comprehensive error handling is essential for building resilient AWS Lambda functions. This guide -covers exception handling patterns, cancellation token usage, lifecycle error handling, retry -strategies, and AWS Lambda integration patterns specific to the aws-lambda-host framework. +`AwsLambda.Host` does not hide exceptions—it embraces the standard .NET model so you can decide where +to intercept failures. This guide explains how errors flow through handlers, middleware, and lifecycle +hooks so you can add the right amount of protection without fighting the framework. ---- +## Invocation Errors: What Happens by Default -## Introduction - -AWS Lambda functions face unique error handling challenges: - -- **Timeout constraints**: Lambda functions must complete within a configured timeout -- **Cold starts**: Initialization failures prevent the function from starting -- **Graceful shutdown**: Limited time window for cleanup before container termination -- **Retry behavior**: Event source determines retry semantics - -The aws-lambda-host framework provides built-in error handling mechanisms while allowing you to -implement custom error handling strategies. - ---- - -## Handler Exception Handling - -### Exception Bubbling - -Exceptions thrown in Lambda handlers naturally bubble up through the middleware pipeline. This -allows middleware to intercept, log, or transform exceptions before they reach the Lambda runtime. - -```csharp title="Program.cs" -lambda.MapHandler(async ([Event] OrderRequest request, IOrderService service) => -{ - // Unhandled exceptions bubble through middleware to Lambda runtime - return await service.ProcessAsync(request); -}); -``` - -**How it works**: - -1. Handler throws exception -2. Exception propagates through middleware pipeline (innermost to outermost) -3. Each middleware can catch, log, or re-throw -4. If unhandled, Lambda runtime receives the exception - -### Try-Catch in Handlers - -For fine-grained error handling, use try-catch blocks to handle specific exception types and return -error responses. +- Handlers run inside the middleware pipeline. If a handler throws and nothing catches the exception, + `AwsLambda.Host` lets it bubble back through the pipeline. +- Once the exception leaves the outermost middleware, the AWS .NET Lambda runtime records the error, + writes it to CloudWatch Logs, and returns a failed invocation (with retries governed by the event + source). +- Because the runtime already reports unhandled errors, you only need to add custom handling when you + want different logging, metrics, or response shaping. ```csharp title="Program.cs" linenums="1" -lambda.MapHandler(async ([Event] OrderRequest request, IOrderService service) => -{ - try - { - var result = await service.ProcessAsync(request); - return new OrderResponse - { - OrderId = result.Id, - Status = "Success" - }; - } - catch (ValidationException ex) - { - // Return structured error response for validation failures - return new OrderResponse - { - Status = "ValidationError", - Error = ex.Message - }; - } - catch (InvalidOperationException ex) - { - // Log and return business logic errors - Console.WriteLine($"Business logic error: {ex.Message}"); - return new OrderResponse - { - Status = "BusinessError", - Error = "Unable to process order" - }; - } - // Let other exceptions bubble to middleware/runtime -}); +lambda.MapHandler(([Event] OrderRequest request, IOrderService service) => + service.ProcessAsync(request) // unhandled exception flows to Lambda runtime +); ``` -!!! tip "When to catch exceptions in handlers" - -- Validation errors that should return specific HTTP status codes or error structures -- Expected business logic exceptions (insufficient inventory, duplicate orders, etc.) -- Errors requiring custom response formatting - ---- +## Prefer Middleware for Error Handling -## Middleware Error Handling - -Middleware is ideal for global error handling policies that apply to all invocations. - -### Global Error Handler Pattern +Wrapping the pipeline once keeps error handling consistent across every trigger. The snippet below +demonstrates a simple pattern: capture known exceptions, map them to responses (or metrics), and +allow everything else to bubble so Lambda reports the failure. ```csharp title="Program.cs" linenums="1" lambda.UseMiddleware(async (context, next) => @@ -105,861 +35,94 @@ lambda.UseMiddleware(async (context, next) => } catch (ValidationException ex) { - // Log validation errors with context - Console.WriteLine($"[Validation Error] RequestId: {context.LambdaContext.AwsRequestId}, Error: {ex.Message}"); - - // Set error response (if using API Gateway integration) - // context.Items["StatusCode"] = 400; - // context.Items["ErrorMessage"] = ex.Message; - - // Re-throw to Lambda runtime (which may have retry logic) - throw; - } - catch (OperationCanceledException) - { - // Log timeout/cancellation - Console.WriteLine($"[Timeout] RequestId: {context.LambdaContext.AwsRequestId} cancelled"); - throw; - } - catch (Exception ex) - { - // Log unexpected errors with full stack trace - Console.WriteLine($"[Unexpected Error] RequestId: {context.LambdaContext.AwsRequestId}"); - Console.WriteLine($"Exception: {ex}"); - - // Optional: Send to error tracking service (Sentry, Rollbar, etc.) - // await errorTracker.CaptureAsync(ex); + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogWarning(ex, "Validation failed for {RequestId}", context.AwsRequestId); + // Optional: set a response via feature/envelope APIs instead of rethrowing. throw; } -}); -``` - -### Error Transformation Middleware - -Transform exceptions into structured error responses before they reach the Lambda runtime. - -```csharp title="Program.cs" linenums="1" -lambda.UseMiddleware(async (context, next) => -{ - try - { - await next(context); - } - catch (ValidationException ex) - { - // Transform validation exception into error response - var errorResponse = new ErrorResponse - { - Type = "ValidationError", - Message = ex.Message, - RequestId = context.LambdaContext.AwsRequestId, - Timestamp = DateTime.UtcNow - }; - - // Serialize error response to context - var responseFeature = context.Features.Get(); - if (responseFeature != null) - { - // Set error response (implementation depends on response handling) - context.Items["ErrorResponse"] = errorResponse; - } - - // Don't re-throw - error has been handled - } - catch (Exception ex) + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) { - // Log and re-throw unhandled errors - Console.WriteLine($"Unhandled error: {ex}"); + // Lambda is about to time out; log and rethrow so the runtime records the failure. throw; } }); ``` -!!! warning "Middleware execution order matters" -Error handling middleware should typically be registered first (outer layer) so it can catch -exceptions from all subsequent middleware and handlers. - ---- - -## Cancellation Token Handling - -AWS Lambda enforces strict timeout limits. The framework provides automatic cancellation token -management to ensure your code can gracefully handle timeouts. - -### InvocationCancellationBuffer - -The `InvocationCancellationBuffer` configuration prevents hard Lambda timeouts by firing the -cancellation token before Lambda terminates your function. - -**How it works**: - -``` -Lambda Timeout = 30 seconds -InvocationCancellationBuffer = 3 seconds (default) ------------------------- -CancellationToken fires at: 27 seconds -Lambda hard timeout at: 30 seconds -``` - -This gives your code 3 seconds to complete current operations, flush metrics, and gracefully exit -before Lambda forcefully terminates the container. - -```csharp title="Program.cs" linenums="1" -builder.Services.ConfigureLambdaHostOptions(options => -{ - // Default is 3 seconds - options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); -}); -``` - -### Timeout Calculation - -From `DefaultLambdaCancellationFactory`: - -```csharp -var maxAllowedDuration = context.RemainingTime - _bufferDuration; - -if (maxAllowedDuration <= TimeSpan.Zero) -{ - throw new InvalidOperationException( - "CancellationTokenSource provided with insufficient time. " - + $"Lambda Remaining Time = {context.RemainingTime:c}, " - + $"Cancellation Token Buffer = {_bufferDuration:c}, " - + $"Candidate Token Duration = {maxAllowedDuration:c}" - ); -} -``` - -!!! danger "Insufficient time error" -If `InvocationCancellationBuffer` exceeds the Lambda's remaining time, the framework throws -`InvalidOperationException` with diagnostic information. This typically occurs when: - - - Lambda timeout is very short (< 3 seconds) - - Buffer is configured larger than Lambda timeout - - Previous operations consumed too much time - -### Respecting Cancellation Tokens - -Always pass and respect cancellation tokens in async operations: - -```csharp title="Program.cs" linenums="1" -lambda.MapHandler(async ( - [Event] OrderRequest request, - IOrderService orderService, - IHttpClientFactory httpClientFactory, - CancellationToken cancellationToken) => -{ - // Pass cancellation token to all async operations - var order = await orderService.CreateOrderAsync(request, cancellationToken); - - // HTTP calls should respect cancellation - var httpClient = httpClientFactory.CreateClient(); - var response = await httpClient.PostAsJsonAsync( - "https://api.example.com/notify", - order, - cancellationToken - ); - - return new OrderResponse { OrderId = order.Id }; -}); -``` - -### Handling OperationCanceledException - -When a cancellation token fires, async operations throw `OperationCanceledException`: - -```csharp title="Program.cs" linenums="1" -lambda.MapHandler(async ( - [Event] ProcessingRequest request, - IDataService dataService, - CancellationToken cancellationToken) => -{ - try - { - var data = await dataService.ProcessLargeDatasetAsync(request.DatasetId, cancellationToken); - return new ProcessingResponse { Status = "Completed", RecordsProcessed = data.Count }; - } - catch (OperationCanceledException) - { - // Timeout occurred - return partial results or failure status - Console.WriteLine($"Processing timeout for dataset {request.DatasetId}"); - return new ProcessingResponse - { - Status = "Timeout", - RecordsProcessed = dataService.GetProcessedCount() - }; - } -}); -``` - -!!! tip "Best practice for long-running operations" - -- Check `cancellationToken.IsCancellationRequested` periodically in loops -- Pass cancellation tokens to all I/O operations (HTTP, database, file operations) -- Catch `OperationCanceledException` to handle graceful degradation -- Flush any buffered data before returning on cancellation +**Recommendations** ---- +- Register error-handling middleware first so it wraps every other component. +- Use the helper extensions (`context.GetResponse()`, `context.GetEvent()`, etc.) from + `FeatureLambdaHostContextExtensions` (they wrap `ILambdaHostContext.Features`) when you need to read + or replace the outgoing payload instead of throwing. +- Still rethrow fatal errors so the runtime produces accurate CloudWatch metrics and DLQ/SQS retries. -## Lifecycle Error Handling +## Handler-Level Try/Catch -The OnInit and OnShutdown lifecycle phases have specialized error handling behavior. - -### OnInit Error Handling - -**Behavior**: - -- All OnInit handlers execute **in parallel** -- Handlers have an `InitTimeout` (default 5 seconds) -- Each handler gets its own **service scope** -- Exceptions are caught and **aggregated** -- Handlers can return `false` to **abort startup without throwing** -- All handlers complete, then errors are bundled into `AggregateException` +Keep handlers thin, but do catch exceptions when you want a different payload or domain-specific +logic. Leave everything else to your middleware/global policy. ```csharp title="Program.cs" linenums="1" -lambda.OnInit(async (ICache cache, CancellationToken cancellationToken) => +lambda.MapHandler(async ([Event] CheckoutRequest request, ICheckoutService service) => { try { - // Warm up cache during cold start - await cache.WarmUpAsync(cancellationToken); - return true; // Startup succeeds + return await service.RunAsync(request); } - catch (OperationCanceledException) + catch (PaymentDeclinedException ex) { - // InitTimeout fired (default 5 seconds) - Console.WriteLine("Cache warmup timeout"); - return false; // Abort startup - prevents Lambda from starting + return new CheckoutResponse("Declined", ex.Message); } - catch (Exception ex) - { - // Log initialization error - Console.WriteLine($"Cache warmup failed: {ex.Message}"); - - // Option 1: Return false to abort startup gracefully - return false; - - // Option 2: Throw to include in AggregateException - // throw; - } -}); - -lambda.OnInit(async (IDatabaseMigrator migrator, CancellationToken cancellationToken) => -{ - // Second handler - runs in parallel with first - await migrator.EnsureMigratedAsync(cancellationToken); - return true; -}); -``` - -**Configuration**: - -```csharp title="Program.cs" -builder.Services.ConfigureLambdaHostOptions(options => -{ - // Maximum time for all OnInit handlers (default 5 seconds) - options.InitTimeout = TimeSpan.FromSeconds(10); }); ``` -**Error scenarios**: - -| Scenario | Handler 1 | Handler 2 | Result | -|-------------------|--------------------|--------------------|-------------------------------------------| -| Both succeed | `return true` | `return true` | Lambda starts | -| One returns false | `return false` | `return true` | Lambda aborts (no exception) | -| One throws | throws `Exception` | `return true` | `AggregateException` thrown | -| Both throw | throws `Exception` | throws `Exception` | `AggregateException` with both exceptions | - -!!! note "Parallel execution" -From `LambdaOnInitBuilder.cs`: - -```csharp -var tasks = _handlers.Select(h => RunInitHandler(h, cts.Token)); -var results = await Task.WhenAll(tasks).ConfigureAwait(false); -``` +## Lifecycle Hooks (OnInit / OnShutdown) -All init handlers run concurrently, not sequentially. +`LambdaApplication` runs OnInit and OnShutdown handlers outside the invocation pipeline, but each +handler executes in its own DI scope and errors are aggregated: -### OnShutdown Error Handling - -**Behavior**: - -- All OnShutdown handlers execute **in parallel** -- Each handler gets its own **service scope** -- Individual exceptions are caught and collected -- All handlers complete before throwing `AggregateException` -- **Does not interrupt execution of other handlers** +- **OnInit** – All handlers run concurrently with a token derived from + `LambdaHostOptions.InitTimeout`. Each handler can optionally return `bool`. `AwsLambda.Host` collects + every exception and throws an `AggregateException("Encountered errors while running OnInit handlers", …)` + if any fail. If a handler returns `false`, initialization aborts even when no exception occurred. +- **OnShutdown** – Handlers also run concurrently. Any exception is captured and rethrown as an + `AggregateException("Encountered errors while running OnShutdown handlers", …)` after every handler + finishes (or faults). Use the provided `CancellationToken` to respect the remaining shutdown window. ```csharp title="Program.cs" linenums="1" -lambda.OnShutdown(async (IMetricsService metrics, CancellationToken cancellationToken) => +lambda.OnInit(async (ICache cache, CancellationToken ct) => { - try - { - // Flush metrics before shutdown - await metrics.FlushAsync(cancellationToken); - } - catch (Exception ex) - { - // Log shutdown error - won't prevent other handlers from running - Console.WriteLine($"Metrics flush error: {ex.Message}"); - - // Error is aggregated but other shutdown handlers continue - throw; - } + await cache.WarmAsync(ct); + return true; // omit or return false to abort startup }); -lambda.OnShutdown(async (IConnectionPool connectionPool, CancellationToken cancellationToken) => -{ - // Runs in parallel with metrics flush - await connectionPool.DrainAsync(cancellationToken); -}); -``` - -**Shutdown timing configuration**: - -```csharp title="Program.cs" linenums="1" -builder.Services.ConfigureLambdaHostOptions(options => -{ - // Time between SIGTERM and SIGKILL (default 500ms for external extensions) - options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500ms - - // options.ShutdownDuration = ShutdownDuration.InternalExtensions; // 300ms - // options.ShutdownDuration = ShutdownDuration.NoExtensions; // 0ms - - // Buffer to ensure cleanup completes (default 50ms) - options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); -}); -``` - -**Shutdown timeline**: - -``` -1. AWS sends SIGTERM -2. ShutdownDuration starts (500ms for ExternalExtensions) -3. CancellationToken fires at (ShutdownDuration - ShutdownDurationBuffer) = 450ms -4. OnShutdown handlers execute with 450ms to complete -5. AWS sends SIGKILL at 500ms (forceful termination) -``` - -!!! warning "Limited shutdown time" -Shutdown handlers must complete quickly (typically < 500ms). Long-running cleanup operations may be -terminated by SIGKILL. Use cancellation tokens to detect imminent shutdown. - ---- - -## Application-Level Retry Patterns - -The aws-lambda-host framework **does not provide built-in retry mechanisms**. You implement retry -logic in your application code or use libraries like Polly. - -### Simple Exponential Backoff - -```csharp title="RetryHelper.cs" linenums="1" -public static class RetryHelper +lambda.OnShutdown(async (ITelemetry telemetry, CancellationToken ct) => { - public static async Task RetryAsync( - Func> operation, - int maxRetries = 3, - CancellationToken cancellationToken = default) - { - for (int i = 0; i < maxRetries; i++) - { - try - { - return await operation(); - } - catch (Exception ex) when (i < maxRetries - 1 && IsTransientError(ex)) - { - var delay = TimeSpan.FromSeconds(Math.Pow(2, i)); // 1s, 2s, 4s - Console.WriteLine($"Retry {i + 1}/{maxRetries} after {delay.TotalSeconds}s due to: {ex.Message}"); - await Task.Delay(delay, cancellationToken); - } - } - - throw new Exception($"Operation failed after {maxRetries} retries"); - } - - private static bool IsTransientError(Exception ex) - { - return ex is HttpRequestException or TimeoutException or IOException; - } -} -``` - -**Usage**: - -```csharp title="Program.cs" -lambda.MapHandler(async ([Event] Request request, IHttpClientFactory httpClientFactory, CancellationToken ct) => -{ - var httpClient = httpClientFactory.CreateClient(); - - var response = await RetryHelper.RetryAsync(async () => - await httpClient.GetAsync($"https://api.example.com/data/{request.Id}", ct), - maxRetries: 3, - cancellationToken: ct - ); - - return await response.Content.ReadAsStringAsync(ct); + await telemetry.FlushAsync(ct); // exceptions are aggregated and rethrown }); ``` -### Using Polly +Because the host rethrows aggregate exceptions, CloudWatch logs clearly show every failing handler. -[Polly](https://github.com/App-vNext/Polly) is the recommended library for resilience and transient -fault handling. +## Timeouts and Cancellation -**Installation**: - -```bash -dotnet add package Polly -``` - -**Retry policy**: +`AwsLambda.Host` links invocation tokens to the Lambda timeout using +`LambdaHostOptions.InvocationCancellationBuffer`. Honor that token in services and middleware—if your +code catches `OperationCanceledException`, log and rethrow so the runtime still marks the invocation +as failed rather than silently succeeding. ```csharp title="Program.cs" linenums="1" -using Polly; -using Polly.Retry; - -// Configure resilience in service registration -builder.Services.AddSingleton(sp => -{ - return Policy - .Handle() - .Or() - .WaitAndRetryAsync( - retryCount: 3, - sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - onRetry: (exception, timeSpan, retryCount, context) => - { - Console.WriteLine($"Retry {retryCount} after {timeSpan.TotalSeconds}s due to {exception.Message}"); - } - ); -}); - -// Use in handler -lambda.MapHandler(async ( - [Event] Request request, - IHttpClientFactory httpClientFactory, - AsyncRetryPolicy retryPolicy, - CancellationToken ct) => -{ - var httpClient = httpClientFactory.CreateClient(); - - var response = await retryPolicy.ExecuteAsync(async () => - await httpClient.GetAsync($"https://api.example.com/data/{request.Id}", ct) - ); - - return await response.Content.ReadAsStringAsync(ct); -}); -``` - -**Advanced Polly policies**: - -```csharp title="Program.cs" linenums="1" -using Polly; -using Polly.CircuitBreaker; - -// Circuit breaker policy -var circuitBreakerPolicy = Policy - .Handle() - .CircuitBreakerAsync( - handledEventsAllowedBeforeBreaking: 3, - durationOfBreak: TimeSpan.FromSeconds(30) - ); - -// Timeout policy -var timeoutPolicy = Policy - .TimeoutAsync(TimeSpan.FromSeconds(10)); - -// Combine policies (timeout → retry → circuit breaker) -var combinedPolicy = Policy.WrapAsync(circuitBreakerPolicy, retryPolicy, timeoutPolicy); - -await combinedPolicy.ExecuteAsync(async () => - await httpClient.GetAsync(url, cancellationToken) -); -``` - -!!! tip "Retry best practices" - -- Only retry **transient errors** (network failures, timeouts, 5xx HTTP responses) -- **Don't retry** validation errors, authentication failures, or 4xx responses -- Use **exponential backoff** to avoid overwhelming downstream services -- Add **jitter** to prevent thundering herd problem -- Respect **Lambda timeout** - ensure retries complete before cancellation - ---- - -## AWS Lambda Integration - -### Event Source Retries - -AWS Lambda provides built-in retry mechanisms for asynchronous event sources: - -| Event Source | Retry Behavior | -|--------------------------------|-------------------------------------------------------------------------------------------------------------------| -| **SQS** | Configurable `maxReceiveCount` on queue. After retries exhausted, message moves to DLQ (if configured). | -| **EventBridge** | Automatic retries for up to 24 hours with exponential backoff. | -| **Kinesis / DynamoDB Streams** | Retries until record expires or processed successfully. Use `BisectBatchOnFunctionError` to isolate poison pills. | -| **SNS** | Retries twice with exponential backoff. After failure, message discarded (configure DLQ on SNS topic). | -| **S3** | No automatic retries. Use S3 Event Notifications with SQS for retry capability. | - -**Synchronous invocations** (API Gateway, ALB, Lambda Function URLs): - -- **No automatic retries** - client must retry -- Errors return immediately to caller -- Use application-level retries (Polly) or API Gateway retry configuration - -### Dead Letter Queues (DLQ) - -DLQs are configured **at the AWS Lambda level**, not in the application code. - -**Asynchronous invocations**: - -```yaml title="template.yaml (SAM)" linenums="1" -Resources: - MyFunction: - Type: AWS::Serverless::Function - Properties: - Handler: bootstrap - Runtime: provided.al2023 - DeadLetterQueue: - Type: SQS - TargetArn: !GetAtt MyFunctionDLQ.Arn - - MyFunctionDLQ: - Type: AWS::SQS::Queue - Properties: - QueueName: MyFunction-DLQ - MessageRetentionPeriod: 1209600 # 14 days -``` - -**SQS event source**: - -```yaml title="template.yaml (SAM)" linenums="1" -Resources: - MyQueue: - Type: AWS::SQS::Queue - Properties: - QueueName: MyQueue - RedrivePolicy: - deadLetterTargetArn: !GetAtt MyQueueDLQ.Arn - maxReceiveCount: 3 # Retry 3 times before DLQ - - MyQueueDLQ: - Type: AWS::SQS::Queue - Properties: - QueueName: MyQueue-DLQ -``` - -**Monitoring DLQs**: - -```yaml title="template.yaml (SAM)" linenums="1" -Resources: - DLQAlarm: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmName: MyFunction-DLQ-Alarm - MetricName: ApproximateNumberOfMessagesVisible - Namespace: AWS/SQS - Statistic: Sum - Period: 300 - EvaluationPeriods: 1 - Threshold: 1 - ComparisonOperator: GreaterThanOrEqualToThreshold - Dimensions: - - Name: QueueName - Value: !GetAtt MyFunctionDLQ.QueueName - AlarmActions: - - !Ref AlertTopic -``` - ---- - -## Best Practices - -### ✅ Do - -- **Use specific exception types** to distinguish error categories (validation, business logic, - infrastructure) -- **Log errors with context**: Request IDs, correlation IDs, input parameters (sanitized) -- **Respect cancellation tokens**: Pass to all async operations, check `IsCancellationRequested` in - loops -- **Implement retries for transient failures**: Network errors, timeouts, 5xx HTTP responses -- **Use middleware for global error handling**: Logging, metrics, error tracking service integration -- **Return structured error responses**: Consistent error format for API clients -- **Configure appropriate `InvocationCancellationBuffer`**: Balance between graceful handling and - Lambda timeout -- **Handle `OperationCanceledException` gracefully**: Return partial results or meaningful error - response -- **Monitor DLQs with CloudWatch alarms**: Get notified when messages accumulate -- **Use OnInit return value for graceful abort**: Return `false` instead of throwing when startup - should abort - -### ❌ Don't - -- **Don't swallow exceptions without logging**: Silent failures make debugging impossible -- **Don't ignore cancellation tokens**: Leads to hard Lambda timeouts and incomplete work -- **Don't retry non-transient errors**: Validation errors, authentication failures, 4xx responses -- **Don't let OnInit timeout**: Keep initialization fast (< 5 seconds by default) -- **Don't assume OnShutdown always completes**: AWS sends SIGKILL after ShutdownDuration -- **Don't use long shutdown operations**: Limited time window (typically < 500ms) -- **Don't log sensitive data**: Sanitize PII, credentials, and secrets before logging -- **Don't retry indefinitely**: Respect Lambda timeout, use exponential backoff with max retries - ---- - -## Anti-Patterns to Avoid - -### ❌ Ignoring Cancellation Tokens - -```csharp title="❌ Bad - No cancellation token" -lambda.MapHandler(async ([Event] Request request, IDataService dataService) => -{ - // This operation can't be cancelled - will continue until Lambda timeout - var data = await dataService.ProcessLargeDatasetAsync(request.DatasetId); - return new Response { Data = data }; -}); -``` - -```csharp title="✅ Good - Respects cancellation" -lambda.MapHandler(async ( - [Event] Request request, - IDataService dataService, - CancellationToken cancellationToken) => -{ - try - { - var data = await dataService.ProcessLargeDatasetAsync(request.DatasetId, cancellationToken); - return new Response { Data = data }; - } - catch (OperationCanceledException) - { - // Handle timeout gracefully - return new Response { Status = "Timeout", PartialData = dataService.GetPartialResults() }; - } -}); -``` - -### ❌ OnInit Timeout Loops - -```csharp title="❌ Bad - Infinite loop ignores timeout" -lambda.OnInit(async (CancellationToken cancellationToken) => +lambda.MapHandler(async (CancellationToken ct) => { - while (true) // Will timeout after InitTimeout (default 5s) - { - await Task.Delay(100); - } - return true; -}); -``` - -```csharp title="✅ Good - Respects cancellation token" -lambda.OnInit(async (ICache cache, CancellationToken cancellationToken) => -{ - try - { - await cache.WarmUpAsync(cancellationToken); - return true; - } - catch (OperationCanceledException) - { - Console.WriteLine("Init timeout - cache warmup incomplete"); - return false; // Abort startup gracefully - } + await DoWorkAsync(ct); + // do not swallow OperationCanceledException unless you intentionally handled the timeout }); ``` -### ❌ Retrying Non-Transient Errors - -```csharp title="❌ Bad - Retries validation errors" -for (int i = 0; i < 3; i++) -{ - try - { - return await orderService.CreateOrderAsync(order); - } - catch (Exception) // Catches ALL exceptions including validation errors - { - await Task.Delay(1000); // Wastes time retrying invalid input - } -} -``` - -```csharp title="✅ Good - Only retries transient errors" -for (int i = 0; i < 3; i++) -{ - try - { - return await orderService.CreateOrderAsync(order); - } - catch (ValidationException) - { - throw; // Don't retry validation errors - } - catch (HttpRequestException) when (i < 2) - { - await Task.Delay(1000); // Retry transient network errors - } -} -``` - ---- - -## Troubleshooting - -### "CancellationTokenSource provided with insufficient time" - -**Error message**: - -``` -InvalidOperationException: CancellationTokenSource provided with insufficient time. -Lambda Remaining Time = 00:00:02, Cancellation Token Buffer = 00:00:03, Candidate Token Duration = -00:00:01 -``` - -**Cause**: `InvocationCancellationBuffer` exceeds Lambda's remaining time. - -**Solutions**: - -1. **Reduce the buffer**: - ```csharp - builder.Services.ConfigureLambdaHostOptions(options => - { - options.InvocationCancellationBuffer = TimeSpan.FromSeconds(1); - }); - ``` - -2. **Increase Lambda timeout** (in SAM/CDK/Terraform template): - ```yaml - MyFunction: - Type: AWS::Serverless::Function - Properties: - Timeout: 30 # Increase from current value - ``` - -3. **Optimize previous operations** to leave more remaining time. - -### "Encountered errors while running OnInit handlers" - -**Error message**: - -``` -AggregateException: Encountered errors while running OnInit handlers: - ---> InvalidOperationException: Database connection failed - ---> TimeoutException: Cache warmup timed out -``` - -**Cause**: One or more OnInit handlers threw exceptions. - -**Solutions**: - -1. **Check inner exceptions**: - ```csharp - try - { - await lambdaHost.RunAsync(); - } - catch (AggregateException aggEx) - { - foreach (var innerEx in aggEx.InnerExceptions) - { - Console.WriteLine($"Init error: {innerEx.Message}"); - } - } - ``` - -2. **Add error handling to OnInit handlers**: - ```csharp - lambda.OnInit(async (ICache cache, CancellationToken ct) => - { - try - { - await cache.WarmUpAsync(ct); - return true; - } - catch (Exception ex) - { - Console.WriteLine($"Cache warmup failed: {ex.Message}"); - return false; // Abort gracefully instead of throwing - } - }); - ``` - -3. **Increase InitTimeout**: - ```csharp - builder.Services.ConfigureLambdaHostOptions(options => - { - options.InitTimeout = TimeSpan.FromSeconds(10); - }); - ``` - -### "Graceful shutdown of the Lambda function failed" - -**Error message**: - -``` -OperationCanceledException: Graceful shutdown of the Lambda function failed: the bootstrap operation did not complete within the allocated timeout period. -``` - -**Cause**: Bootstrap process didn't complete within shutdown timeout. - -**Solutions**: - -1. **Ensure OnShutdown handlers respect cancellation token**: - ```csharp - lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => - { - try - { - await metrics.FlushAsync(ct); // Must support cancellation - } - catch (OperationCanceledException) - { - Console.WriteLine("Metrics flush cancelled due to shutdown timeout"); - } - }); - ``` - -2. **Reduce OnShutdown work**: - ```csharp - lambda.OnShutdown(async (IMetrics metrics) => - { - // Quick flush - don't wait for acknowledgment - metrics.FlushAndForget(); - await Task.CompletedTask; - }); - ``` - -3. **Increase shutdown duration** (if using external extensions): - ```csharp - builder.Services.ConfigureLambdaHostOptions(options => - { - options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500ms - options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(50); - }); - ``` - ---- - -## Key Takeaways - -1. **Exception handling**: Use middleware for global error policies, handlers for specific error - responses -2. **Cancellation tokens**: Critical for respecting Lambda timeouts and graceful shutdown -3. **InvocationCancellationBuffer**: Prevents hard timeouts by firing cancellation before Lambda - terminates -4. **OnInit errors**: Handlers run in parallel, errors aggregated, can return `false` to abort -5. **OnShutdown errors**: All handlers execute despite individual failures, errors aggregated -6. **Retries**: Framework provides no built-in retry - use Polly or custom logic -7. **DLQs**: Configure at AWS Lambda level for asynchronous event sources -8. **Error logging**: Always include request context (request ID, correlation ID) - ---- - -## Next Steps +## Checklist -- **[Testing](testing.md)** - Learn how to test error handling logic -- **[Lifecycle Management](lifecycle-management.md)** - Deep dive into OnInit and OnShutdown -- **[Configuration](configuration.md)** - Configure timeout and cancellation settings -- **[Deployment](deployment.md)** - Configure DLQs and CloudWatch alarms in infrastructure +- Install error-handling middleware as the outermost component. +- Keep handler try/catch blocks targeted—handle what you expect, rethrow everything else. +- Respect `CancellationToken` in handlers, middleware, and lifecycle hooks. +- Remember that OnInit/OnShutdown aggregate exceptions and rethrow them; log inside the handler for context. +- Rely on AWS retry/DLQ behavior instead of suppressing errors unless you have a compelling reason. diff --git a/docs/guides/handler-registration.md b/docs/guides/handler-registration.md index cb3b2d43..1fe15230 100644 --- a/docs/guides/handler-registration.md +++ b/docs/guides/handler-registration.md @@ -39,7 +39,7 @@ interface IGreetingService ## Handler Signatures and the `[Event]` Parameter -Handlers that receive an incoming payload must identify exactly one parameter with `[Event]`. The generator uses that marker to synthesize deserialization logic (JSON by default, or whatever envelope/serializer is active). If your Lambda does **not** expect input (e.g., scheduled jobs, health checks, etc.), you can omit the `[Event]` attribute entirely—just define a handler with no payload parameter and aws-lambda-host skips the event binding phase. +Handlers that receive an incoming payload must identify exactly one parameter with `[Event]`. The generator uses that marker to synthesize deserialization logic (JSON by default, or whatever envelope/serializer is active). If your Lambda does **not** expect input (e.g., scheduled jobs, health checks, etc.), you can omit the `[Event]` attribute entirely—just define a handler with no payload parameter and `AwsLambda.Host` skips the event binding phase. - `[Event]` may appear on reference types, structs, records, collection types, or envelope types such as `ApiGatewayRequestEnvelope`. - Handlers without payloads can simply omit `[Event]` by not declaring an event parameter at all. diff --git a/docs/guides/index.md b/docs/guides/index.md index eae14790..2cdf9b24 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -1,6 +1,6 @@ # Guides -Comprehensive guides for building production Lambda functions with aws-lambda-host. Each guide provides in-depth coverage of a specific framework feature with complete examples, best practices, and troubleshooting. +Comprehensive guides for building production Lambda functions with `AwsLambda.Host`. Each guide provides in-depth coverage of a specific framework feature with complete examples, best practices, and troubleshooting. ## Core Framework Guides @@ -93,7 +93,7 @@ Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, an ## Learning Path -### New to aws-lambda-host? +### New to `AwsLambda.Host`? Start with [Getting Started](/getting-started/) to build your first Lambda function, then return here for deeper coverage of specific features. diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index c9df321b..19708807 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -123,7 +123,7 @@ Common features: ### Feature Providers in Practice -When `context.Features.Get()` runs, aws-lambda-host walks through every registered `IFeatureProvider` +When `context.Features.Get()` runs, `AwsLambda.Host` walks through every registered `IFeatureProvider` until one returns the requested feature. Built-in providers handle common cases such as response serialization. Use the same pattern for your features. diff --git a/docs/index.md b/docs/index.md index 1d997555..5d492217 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,7 +65,7 @@ Stop writing boilerplate Lambda code. Start building features with patterns you ``` -=== "aws-lambda-host" +=== "`AwsLambda.Host`" ```csharp // ✅ Familiar .NET Core builder pattern From 5efa22a9699b43d963d569475b896834c800591f Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 16:09:27 -0500 Subject: [PATCH 57/65] feat(docs): add comprehensive hosting guide for Lambda applications - Introduced `hosting.md` to provide an in-depth guide on hosting and application building in Lambda. - Explained default behavior of `LambdaApplication.CreateBuilder()` with detailed feature breakdown. - Added examples for builder customization using `LambdaApplicationOptions` and service registration. - Documented runtime execution flow, middleware usage, lifecycle hooks, and troubleshooting tips. - Provided testing scenarios and alternate hosting approaches for enhanced application flexibility. --- docs/guides/hosting.md | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/guides/hosting.md diff --git a/docs/guides/hosting.md b/docs/guides/hosting.md new file mode 100644 index 00000000..2bbb7e0c --- /dev/null +++ b/docs/guides/hosting.md @@ -0,0 +1,116 @@ +# Hosting & Application Builder + +`LambdaApplication.CreateBuilder()` mirrors ASP.NET Core’s `WebApplication.CreateBuilder()` but is tuned for Lambda’s execution model. This guide explains what the builder does for you, how to customize it, and how the hosting infrastructure composes lifecycle hooks, middleware, and handlers. + +## Builder Defaults + +Calling `LambdaApplication.CreateBuilder()` assembles a standard .NET host with Lambda-friendly defaults: + +- **Configuration sources** – Adds `appsettings.json`, `appsettings.{Environment}.json`, user secrets (development), `AWS_` and `DOTNET_` prefixed environment variables, and ambient environment variables. The resulting `ConfigurationManager` is exposed via `builder.Configuration`. +- **Environment & content root** – Sets `IHostEnvironment.ApplicationName` from `AWS_LAMBDA_FUNCTION_NAME` (when available) and resolves the content root by honoring `DOTNET_CONTENTROOT`, `AWS_LAMBDA_TASK_ROOT`, or falling back to `Directory.GetCurrentDirectory()`. +- **Logging** – Registers console logging with activity tracking enabled. In Development, scope validation is turned on so singleton/scoped misuse throws during build. +- **Dependency injection** – Every call to `builder.Services` hits the standard `IServiceCollection`. On `builder.Build()`, aws-lambda-host registers: + - `ILambdaInvocationBuilderFactory`, `ILambdaOnInitBuilderFactory`, and `ILambdaOnShutdownBuilderFactory` so lambda-specific pipelines can be composed later. + - `LambdaHostedService`, `ILambdaHandlerFactory`, feature collections, and `ILambdaBootstrapOrchestrator`. + - Default implementations of `ILambdaSerializer` (System.Text.Json) and `ILambdaCancellationFactory` unless you already registered your own via `TryAddLambdaHostDefaultServices()`. + +Most applications can rely entirely on `CreateBuilder()` + `builder.Build()`—just add services, middleware, handlers, and call `await lambda.RunAsync();`. + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddSingleton(); + +var lambda = builder.Build(); + +lambda.MapHandler(([Event] string name, IGreetingService service) => + service.Greet(name) +); + +await lambda.RunAsync(); +``` + +## Customizing the Builder + +Use `LambdaApplicationOptions` when you need to tweak how the builder initializes: + +```csharp title="Program.cs" linenums="1" +var builder = LambdaApplication.CreateBuilder(new LambdaApplicationOptions +{ + DisableDefaults = true, // start from an empty HostApplicationBuilder + Configuration = new ConfigurationManager(), // seed your own configuration sources + ContentRootPath = "/var/task", // explicitly set the content root + EnvironmentName = Environments.Production, +}); +``` + +When `DisableDefaults = true`, you’re responsible for adding configuration providers, logging, and environment metadata before calling `builder.Build()`. This mode is useful for specialized hosting scenarios (custom service provider factory, integration tests, CLI applications) but most Lambda projects should keep the defaults. + +Other customization hooks: + +- `LambdaApplicationOptions.Args` – Flow command-line arguments into configuration. +- `builder.Services.ConfigureLambdaHostOptions(...)` – Override runtime behavior (Init/Shutdown timeouts, invocation cancellation buffer, output formatting). +- `builder.Services.AddLambdaSerializerWithContext()` – Swap the default serializer with a source-generated one (or register any `ILambdaSerializer` manually). +- Register an `ILambdaHostContextAccessor` if you need to resolve `ILambdaHostContext` outside handlers/middleware. + +## Build Phase + +`builder.Build()` finalizes configuration and returns a `LambdaApplication` that implements `IHost`, `ILambdaInvocationBuilder`, `ILambdaOnInitBuilder`, and `ILambdaOnShutdownBuilder`. During build: + +1. `TryAddLambdaHostDefaultServices()` ensures a serializer and cancellation factory exist. +2. A standard `IHost` is constructed via `Host.CreateEmptyApplicationBuilder` + your service registrations. +3. The `LambdaApplication` wrapper caches factories for the invocation, init, and shutdown builders. + +After build you can still call: + +- `lambda.UseMiddleware(...)`, `lambda.MapHandler(...)` – Compose the invocation pipeline. +- `lambda.OnInit(...)`, `lambda.OnShutdown(...)` – Register lifecycle hooks. +- `lambda.Properties[...]` – Store metadata consumed by middleware or handlers. + +## Runtime Execution + +The `LambdaHostedService` orchestrates execution when you call `await lambda.RunAsync();`: + +1. The hosted service creates a linked `CancellationTokenSource` so it can cancel the loop during shutdown. +2. Middleware and the handler delegate are composed via `ILambdaHandlerFactory`, which calls `ConfigureHandlerBuilder` (injecting your registered middleware and the default envelope middleware). +3. OnInit and OnShutdown builders are configured, including the optional `ClearLambdaOutputFormatting` handler. +4. The bootstrap (`ILambdaBootstrapOrchestrator`) starts polling the Lambda Runtime API, invoking the composed handler for each event, and honoring `LambdaHostOptions` (cancellation buffers, shutdown durations). +5. When the runtime stops, the hosted service calls the aggregated shutdown handler and bubbles exceptions as `AggregateException` so you see every failure in logs. + +## Testing & Alternate Hosts + +Because `LambdaApplication` implements `IHost`, you can start it outside Lambda, resolve services, and +stop it like any other generic host: + +```csharp title="Program.cs" linenums="1" +await lambda.StartAsync(); + +// Resolve services or invoke handlers manually +var client = lambda.Services.GetRequiredService(); +await client.CallAsync(); + +await lambda.StopAsync(); +``` + +For non-Lambda entry points (console apps, acceptance tests), combine `StartAsync`/`StopAsync` with +`DisableDefaults = true` if you want a trimmed-down host that reuses Lambda-ready middleware and +handlers. + +## Troubleshooting + +| Issue | Cause | Fix | +|-------|-------|-----| +| `InvalidOperationException: Lambda Handler is not set.` | `builder.Build()` succeeded but `lambda.MapHandler(...)` was never called. | Register a handler before calling `lambda.RunAsync()`. +| `AggregateException: Encountered errors while running OnInit handlers` | An OnInit delegate threw or returned `false`. | Inspect inner exceptions; ensure handlers honor cancellation and only return `false` for fatal conditions. +| `Graceful shutdown ... did not complete within the allocated timeout` | OnShutdown handlers exceeded `LambdaHostOptions.ShutdownDuration - ShutdownDurationBuffer`. | Reduce work, increase shutdown duration, or skip optional cleanup. +| Environment variables not loaded | You disabled defaults without re-adding `builder.Configuration.AddEnvironmentVariables()`. | Re-add configuration sources or keep defaults. + +## Related Guides + +- **[Dependency Injection](dependency-injection.md)** – Register services and understand lifetimes/scopes. +- **[Lifecycle Management](lifecycle-management.md)** – Deep dive into `OnInit`/`OnShutdown` behavior. +- **[Configuration](configuration.md)** – Bind `LambdaHostOptions` and application settings. +- **[Middleware](middleware.md)** – Build cross-cutting concerns with the invocation pipeline. From 23ef0a120bc72367cc72f970f5b69055fa145b5e Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 16:09:35 -0500 Subject: [PATCH 58/65] feat(docs): enhance envelopes guide with updated examples and improved clarity - Updated `Envelopes` docs with revised definitions and streamlined explanations for key concepts. - Improved table formats for supported event sources and envelope packages. - Added comprehensive examples for `SqsEnvelope` and custom envelope implementations. - Enhanced sections on `EnvelopeOptions` and global serialization configuration. - Introduced best practices for deserialization, error handling, and Native AOT support. --- docs/features/envelopes.md | 601 ++++++------------------------------- 1 file changed, 99 insertions(+), 502 deletions(-) diff --git a/docs/features/envelopes.md b/docs/features/envelopes.md index 0f258bab..d44b9528 100644 --- a/docs/features/envelopes.md +++ b/docs/features/envelopes.md @@ -1,61 +1,60 @@ -# Envelope +# Envelopes -**What are Envelopes?** +Envelope packages extend the official AWS Lambda event types (SQS, SNS, API Gateway, etc.) with +strongly typed payload accessors. Instead of deserializing JSON manually, you work with +`BodyContent`, `MessageContent`, or similar properties that the framework populates before your +handler executes. -Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) and add typed content properties (`BodyContent`, `MessageContent`, etc.) that provide type-safe access to deserialized message payloads. Instead of manually parsing JSON strings from event bodies, you get strongly-typed objects with full IDE support and compile-time type checking. +Behind the scenes, aws-lambda-host injects the `UseExtractAndPackEnvelope` middleware at the end of +every pipeline. That middleware automatically calls `IRequestEnvelope.ExtractPayload` before your +handler runs and `IResponseEnvelope.PackPayload` after it finishes, guaranteeing consistent +serialization for both built-in envelopes and your own custom envelope types. -**Key Benefits**: +## Why Envelopes? -- **Type Safety** - Generic type parameter `` ensures compile-time type checking -- **Extensibility** - Abstract base classes allow custom serialization formats (JSON, XML, etc.) -- **Zero Overhead** - Envelopes extend official AWS event types, adding no runtime cost -- **AOT Ready** - Support for Native AOT compilation via `JsonSerializerContext` registration -- **Familiar API** - Works seamlessly with existing AWS Lambda event patterns +- **Strong typing** – `SqsEnvelope` ensures handlers only run when payloads deserialize into + `Foo`. +- **Zero boilerplate** – No more `JsonSerializer.Deserialize` calls sprinkled through handlers. +- **Consistent serialization** – `EnvelopeOptions` applies globally, including Native AOT + `JsonSerializerContext` support. +- **Extensible** – Implement `IRequestEnvelope`/`IResponseEnvelope` for proprietary event shapes or + alternative serialization formats (XML, Protobuf, etc.). -**Supported Event Sources**: +## Provided Envelopes -| Event Source | Package | NuGet | Use Case | -|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| -| **SQS** | [AwsLambda.Host.Envelopes.Sqs](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sqs) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs) | Queue message processing with type-safe payloads | -| **SNS** | [AwsLambda.Host.Envelopes.Sns](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sns) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns) | Pub/sub notifications with typed messages | -| **API Gateway** | [AwsLambda.Host.Envelopes.ApiGateway](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.ApiGateway) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway) | REST/HTTP/WebSocket APIs with request/response envelopes | -| **Kinesis Data Streams** | [AwsLambda.Host.Envelopes.Kinesis](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Kinesis) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis) | Stream processing with typed records | -| **Kinesis Data Firehose** | [AwsLambda.Host.Envelopes.KinesisFirehose](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.KinesisFirehose) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose) | Data transformation with typed payloads | -| **Kafka (MSK or self-managed)** | [AwsLambda.Host.Envelopes.Kafka](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Kafka) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka) | Event streaming with typed messages | -| **CloudWatch Logs** | [AwsLambda.Host.Envelopes.CloudWatchLogs](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.CloudWatchLogs) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs) | Log processing with typed log events | -| **Application Load Balancer** | [AwsLambda.Host.Envelopes.Alb](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Alb) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb) | ALB target integration with request/response envelopes | +Install only the envelopes you need; each one lives in its own NuGet package. -!!! tip "Learn More About Envelopes" - For detailed implementation examples and API documentation, see the individual package README files on GitHub (linked in the table above). +| Event Source | Package | NuGet | +|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| SQS | [AwsLambda.Host.Envelopes.Sqs](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sqs) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs) | +| SNS | [AwsLambda.Host.Envelopes.Sns](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sns) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns) | +| API Gateway / HTTP API | [AwsLambda.Host.Envelopes.ApiGateway](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.ApiGateway) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway) | +| Kinesis Data Streams | [AwsLambda.Host.Envelopes.Kinesis](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Kinesis) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis) | +| Kinesis Data Firehose | [AwsLambda.Host.Envelopes.KinesisFirehose](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.KinesisFirehose) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose) | +| Kafka (MSK / self-managed) | [AwsLambda.Host.Envelopes.Kafka](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Kafka) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka) | +| CloudWatch Logs | [AwsLambda.Host.Envelopes.CloudWatchLogs](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.CloudWatchLogs) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs) | +| Application Load Balancer (ALB) | [AwsLambda.Host.Envelopes.Alb](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Alb) | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb) | -## Quick Start +Each package ships with README examples in the repository if you need event-specific guidance. -### Installation +## Quick Start -Install only the envelope packages that match the AWS event sources you plan to handle. For example: +Install the envelope package that matches your event source, then use the envelope type in your +handler. This SQS example demonstrates the pattern; swap `SqsEnvelope` with another envelope type +to handle SNS, API Gateway, etc. ```bash -# Core SQS envelope dotnet add package AwsLambda.Host.Envelopes.Sqs - -# API Gateway (REST/WebSocket) envelope -dotnet add package AwsLambda.Host.Envelopes.ApiGateway ``` -Repeat for SNS, Kinesis, Kafka, ALB, etc., as needed. - -### Using Envelopes - -This example demonstrates the envelope pattern using SQS, but the same pattern applies to all envelope types (SNS, API Gateway, Kinesis, etc.) - simply swap `SqsEnvelope` for the appropriate envelope type. - ```csharp title="Program.cs" linenums="1" +using Amazon.Lambda.SQSEvents; using AwsLambda.Host.Builder; using AwsLambda.Host.Envelopes.Sqs; var builder = LambdaApplication.CreateBuilder(); var lambda = builder.Build(); -// Type-safe SQS message processing lambda.MapHandler( ([Event] SqsEnvelope envelope, ILogger logger) => { @@ -64,532 +63,130 @@ lambda.MapHandler( if (record.BodyContent is null) continue; - logger.LogInformation( - "Processing order: {OrderId}", - record.BodyContent.OrderId - ); + logger.LogInformation("Processing order {OrderId}", record.BodyContent.OrderId); } - return new SQSBatchResponse(); + return new SQSBatchResponse(); // optional when you want to signal per-message failures } ); await lambda.RunAsync(); -internal record OrderMessage(string OrderId, decimal Amount); +internal sealed record OrderMessage(string OrderId, decimal Amount); ``` ---- - -## Custom Serialization - -Envelopes support custom serialization formats beyond JSON. By extending envelope base classes, you can implement XML, Protocol Buffers, or any other serialization format. - -**Example use cases:** - -- Legacy systems using XML message formats -- High-performance scenarios requiring Protocol Buffers -- Custom binary formats for specialized domains -- Integration with third-party systems using non-JSON formats - -For implementation examples, see the [SQS Envelope README](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sqs#custom-envelopes) which demonstrates XML serialization. - ---- - -## Configuration - -Envelope configuration is managed through the `EnvelopeOptions` class, which controls how envelope payloads are serialized and deserialized. Configuration applies globally to all envelope types in your application. +## Custom Serialization & EnvelopeOptions -### Quick Start - -The most common configuration scenario is customizing JSON serialization: +All envelope packages respect the global `EnvelopeOptions` configuration. Call +`builder.Services.ConfigureEnvelopeOptions` to tweak `System.Text.Json` behavior, register +`JsonSerializerContext` instances for Native AOT, or enable XML readers/writers for custom envelopes. ```csharp title="Program.cs" linenums="1" using System.Text.Json; - -var builder = LambdaApplication.CreateBuilder(); - -builder.Services.ConfigureEnvelopeOptions(options => -{ - // Use snake_case for JSON property names - options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - - // Case-insensitive property matching - options.JsonOptions.PropertyNameCaseInsensitive = true; - - // Allow trailing commas in JSON - options.JsonOptions.AllowTrailingCommas = true; -}); - -var lambda = builder.Build(); -``` - ---- - -### Configuration Properties - -`EnvelopeOptions` provides five configuration properties for different serialization scenarios: - -| Property | Type | Purpose | Default | When to Use | -|----------|------|---------|---------|-------------| -| **JsonOptions** | `JsonSerializerOptions` | JSON serialization/deserialization for envelope payloads | Empty options | Most common - configure naming policies, converters, AOT support | -| **LambdaDefaultJsonOptions** | `JsonSerializerOptions` | AWS Lambda-specific JSON settings for complex envelope payloads | AWS defaults¹ | Automatic - rarely needs manual configuration | -| **XmlReaderSettings** | `XmlReaderSettings` | XML deserialization settings | Default settings | Custom envelopes using XML serialization | -| **XmlWriterSettings** | `XmlWriterSettings` | XML serialization settings | Default settings | Custom envelopes using XML serialization | -| **Items** | `Dictionary` | Custom extension data | Empty dictionary | Advanced - store custom context for envelope processing | - -¹ *AWS defaults include: case-insensitive property matching, AWS naming policy (PascalCase), DateTime/MemoryStream/ByteArray converters* - -!!! info "LambdaDefaultJsonOptions Behavior" - `LambdaDefaultJsonOptions` is automatically configured with AWS Lambda-compatible settings and is used internally for complex envelope types like SNS-to-SQS and CloudWatch Logs. During post-configuration, the framework automatically copies `JsonOptions.TypeInfoResolver` to `LambdaDefaultJsonOptions` to ensure AOT compatibility works correctly across all envelope types. - ---- - -### JSON Configuration - -Configure JSON serialization for envelope payloads: - -```csharp title="Common JSON patterns" linenums="1" -using System.Text.Json; using System.Text.Json.Serialization; builder.Services.ConfigureEnvelopeOptions(options => { - // Naming policies options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - - // Write indented JSON for debugging - options.JsonOptions.WriteIndented = true; - - // Handle null values options.JsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - - // Number handling - options.JsonOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; - - // Custom converters - options.JsonOptions.Converters.Add(new MyCustomConverter()); -}); -``` - -#### Native AOT Support - -For Native AOT compilation, register a `JsonSerializerContext`: - -```csharp title="AOT-compatible configuration" linenums="1" -using System.Text.Json.Serialization; - -// Define your JSON source generation context -[JsonSerializable(typeof(OrderMessage))] -[JsonSerializable(typeof(SqsEnvelope))] -internal partial class MyJsonContext : JsonSerializerContext { } - -var builder = LambdaApplication.CreateBuilder(); - -builder.Services.ConfigureEnvelopeOptions(options => -{ - // Register source generation context for AOT - options.JsonOptions.TypeInfoResolver = MyJsonContext.Default; + options.JsonOptions.TypeInfoResolver = MyEnvelopeJsonContext.Default; // Native AOT }); - -var lambda = builder.Build(); ``` -!!! tip "AOT Compatibility" - The framework automatically copies `TypeInfoResolver` from `JsonOptions` to `LambdaDefaultJsonOptions`, ensuring all envelope types work correctly with Native AOT compilation. - ---- - -### XML Configuration - -For custom envelopes using XML serialization (see [Custom Serialization](#custom-serialization)): - -```csharp title="XML configuration" linenums="1" -using System.Xml; - -builder.Services.ConfigureEnvelopeOptions(options => -{ - // XML reader settings (deserialization) - options.XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit; - options.XmlReaderSettings.IgnoreWhitespace = true; - options.XmlReaderSettings.IgnoreComments = true; - - // XML writer settings (serialization) - options.XmlWriterSettings.Indent = true; - options.XmlWriterSettings.IndentChars = " "; - options.XmlWriterSettings.OmitXmlDeclaration = false; -}); -``` +The framework automatically copies `JsonOptions.TypeInfoResolver` into the internal +`LambdaDefaultJsonOptions`, so every envelope (including complex ones like CloudWatch Logs) gains the +same serialization metadata. ---- +Need XML or another format? Set `options.XmlReaderSettings` / `XmlWriterSettings` and implement your +envelope using `System.Xml`. See the SQS README in the repo for a complete XML sample. -### Advanced: Custom Extension Data +### Advanced Configuration -The `Items` dictionary allows storing custom context or configuration data for envelope processing: +- **`LambdaDefaultJsonOptions`** – aws-lambda-host maintains a second `JsonSerializerOptions` + instance for Lambda-specific envelopes (e.g., SNS→SQS fan-out). Most apps shouldn’t touch it; the + host copies your `JsonOptions.TypeInfoResolver` automatically. Only override it when you need + different converters for those hybrid envelopes. +- **`Items` dictionary** – Store arbitrary context for custom envelopes: -```csharp title="Using Items dictionary" linenums="1" -builder.Services.ConfigureEnvelopeOptions(options => -{ - // Store custom context for envelope implementations - options.Items["SchemaVersion"] = "2.0"; - options.Items["ValidationEnabled"] = true; - options.Items["CustomProcessor"] = new MyCustomProcessor(); -}); -``` - -**Example: Accessing Items in a custom envelope** - -```csharp title="CustomEnvelope.cs" linenums="1" -public class CustomEnvelope : IRequestEnvelope -{ - public void ExtractPayload(EnvelopeOptions options) + ```csharp title="Program.cs" linenums="1" + builder.Services.ConfigureEnvelopeOptions(options => { - // Access custom configuration - if (options.Items.TryGetValue("ValidationEnabled", out var enabled) - && enabled is true) - { - ValidatePayload(); - } + options.Items["SchemaVersion"] = "v2"; + options.Items["Validator"] = new PayloadValidator(); + }); + ``` - // Deserialize payload... - } -} -``` - ---- + Inside your envelope implementation, read `options.Items` to control validation or routing logic. ## Creating Custom Envelopes -Custom envelopes allow you to define your own event types with nested payloads that are automatically extracted and packed by the framework. This is useful when you need custom event structures beyond AWS's standard event types. - -### When to Use Custom Envelopes - -Create custom envelopes when: +Implement `IRequestEnvelope` and/or `IResponseEnvelope` when you control the event schema or need a +non-standard payload format. The middleware automatically invokes these interfaces, so you only write +the extraction logic. -- ✅ You're defining your own event structure (not using AWS events like SQS, SNS, etc.) -- ✅ Your event contains a serialized payload that needs deserialization -- ✅ You want automatic payload extraction/packing by the framework -- ✅ You need type-safe access to nested event data - -!!! note "Custom Serialization vs Custom Envelopes" - **Custom envelopes** = Your own event types implementing `IRequestEnvelope`/`IResponseEnvelope` - - **Custom serialization** = Using XML, Protobuf, etc. with existing AWS events (see the [SQS Envelope README](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/Envelopes/AwsLambda.Host.Envelopes.Sqs#custom-envelopes) for examples) - -### Core Interfaces - -#### IRequestEnvelope - -Implement this interface for incoming events that contain nested payloads: - -```csharp -public interface IRequestEnvelope -{ - void ExtractPayload(EnvelopeOptions options); -} -``` - -The framework automatically calls `ExtractPayload()` **before** your handler executes, allowing you to deserialize nested data. - -#### IResponseEnvelope - -Implement this interface for outgoing responses that need serialization: - -```csharp -public interface IResponseEnvelope -{ - void PackPayload(EnvelopeOptions options); -} -``` - -The framework automatically calls `PackPayload()` **after** your handler executes, serializing your response data. - ---- - -### Example 1: Simple Request Envelope - -A custom event with metadata and a nested JSON payload: - -```csharp title="CustomRequestEvent.cs" linenums="1" +```csharp title="CustomRequestEnvelope.cs" linenums="1" using System.Text.Json; using System.Text.Json.Serialization; -using AwsLambda.Host.Options; using AwsLambda.Host.Envelopes; +using AwsLambda.Host.Options; -public class CustomRequestEvent : IRequestEnvelope +public sealed class CustomRequestEnvelope : IRequestEnvelope { - [JsonPropertyName("metadata")] - public required EventMetadata Metadata { get; set; } - [JsonPropertyName("payload")] - public required string Payload { get; set; } // Serialized JSON string + public required string Payload { get; set; } [JsonIgnore] - public MyData? PayloadContent { get; set; } // Deserialized object + public MyPayload? PayloadContent { get; private set; } public void ExtractPayload(EnvelopeOptions options) { - PayloadContent = JsonSerializer.Deserialize(Payload, options.JsonOptions); + PayloadContent = JsonSerializer.Deserialize(Payload, options.JsonOptions); } } - -public record EventMetadata(string EventId, DateTime Timestamp); -public record MyData(string Name, int Value); ``` -**Usage:** +In a handler: ```csharp title="Program.cs" linenums="1" -var builder = LambdaApplication.CreateBuilder(); -var lambda = builder.Build(); - -lambda.MapHandler(([Event] CustomRequestEvent request, ILogger logger) => +lambda.MapHandler(([Event] CustomRequestEnvelope envelope) => { - if (request.PayloadContent is null) - { - logger.LogError("Failed to deserialize payload"); + if (envelope.PayloadContent is null) return new { Error = "Invalid payload" }; - } - logger.LogInformation( - "Processing event {EventId}: {Name} = {Value}", - request.Metadata.EventId, - request.PayloadContent.Name, - request.PayloadContent.Value - ); - - return new { Success = true }; + return new { Success = true, envelope.PayloadContent.Name }; }); - -await lambda.RunAsync(); ``` ---- +Response envelopes work the same way—implement `IResponseEnvelope` and serialize into a string +property inside `PackPayload`. -### Example 2: Request/Response Envelope Pair +### Batch Envelopes -Custom envelopes for both incoming requests and outgoing responses: +If your custom event contains multiple records (similar to SQS), deserialize each entry inside +`ExtractPayload`. Keep the original serialized string plus a `[JsonIgnore]` property for the strongly +typed object. -```csharp title="ApiEnvelopes.cs" linenums="1" -using System.Text.Json; -using System.Text.Json.Serialization; -using AwsLambda.Host.Options; -using AwsLambda.Host.Envelopes; +## Best Practices -// Request envelope -public class ApiRequest : IRequestEnvelope -{ - [JsonPropertyName("body")] - public required string Body { get; set; } +- **Check for null** – Always guard against `BodyContent`/`PayloadContent` being `null`. Set it to + `null` if deserialization fails instead of throwing. +- **Use `[JsonIgnore]`** – Keep serialized strings (`Body`, `Payload`, etc.) separate from the + deserialized object to avoid recursive serialization. +- **Return `SQSBatchResponse` when required** – For SQS/SNS to SQS fan-out scenarios, populate + `BatchItemFailures` to signal per-message errors. +- **Centralize configuration** – Prefer `ConfigureEnvelopeOptions` or configuration binding over + ad-hoc serializer tweaks. +- **Log deserialization issues** – Logging helps diagnose malformed payloads without crashing the + Lambda. - [JsonIgnore] - public RequestPayload? BodyContent { get; set; } +## When to Use Envelopes - public void ExtractPayload(EnvelopeOptions options) - { - BodyContent = JsonSerializer.Deserialize(Body, options.JsonOptions); - } -} +Choose envelopes whenever: -// Response envelope -public class ApiResponse : IResponseEnvelope -{ - [JsonPropertyName("statusCode")] - public int StatusCode { get; set; } - - [JsonPropertyName("body")] - public string? Body { get; set; } - - [JsonIgnore] - public ResponsePayload? BodyContent { get; set; } - - public void PackPayload(EnvelopeOptions options) - { - Body = JsonSerializer.Serialize(BodyContent, options.JsonOptions); - } -} - -public record RequestPayload(string Action, Dictionary Parameters); -public record ResponsePayload(bool Success, string Message, object? Data = null); -``` +- You want compile-time type checking for Lambda payloads. +- Your event contains nested JSON/XML that you’d otherwise deserialize manually. +- You need consistent serialization between request and response envelopes (API Gateway, ALB). +- You’re preparing for Native AOT and want the serialization metadata defined in one place. -**Usage:** - -```csharp title="Program.cs" linenums="1" -lambda.MapHandler( - async ([Event] ApiRequest request, CancellationToken cancellationToken) => - { - var response = new ApiResponse { StatusCode = 200 }; - - if (request.BodyContent is null) - { - response.StatusCode = 400; - response.BodyContent = new ResponsePayload( - Success: false, - Message: "Invalid request body" - ); - return response; - } - - // Process the request - var result = await ProcessActionAsync( - request.BodyContent.Action, - request.BodyContent.Parameters, - cancellationToken - ); - - response.BodyContent = new ResponsePayload( - Success: true, - Message: "Action completed", - Data: result - ); - - return response; - } -); -``` - ---- - -### Example 3: Multi-Record Batch Envelope - -A custom event with multiple records, similar to SQS batch processing: - -```csharp title="BatchEvent.cs" linenums="1" -using System.Text.Json; -using System.Text.Json.Serialization; -using AwsLambda.Host.Options; -using AwsLambda.Host.Envelopes; - -public class BatchEvent : IRequestEnvelope -{ - [JsonPropertyName("records")] - public required List Records { get; set; } - - public void ExtractPayload(EnvelopeOptions options) - { - foreach (var record in Records) - { - try - { - record.DataContent = JsonSerializer.Deserialize( - record.Data, - options.JsonOptions - ); - } - catch - { - record.DataContent = null; // Handle deserialization failures gracefully - } - } - } - - public class Record - { - [JsonPropertyName("id")] - public required string Id { get; set; } - - [JsonPropertyName("data")] - public required string Data { get; set; } // Serialized JSON - - [JsonIgnore] - public MyPayload? DataContent { get; set; } // Deserialized object - } -} - -public record MyPayload(string Type, int Count, DateTime CreatedAt); -``` - -**Usage:** - -```csharp title="Program.cs" linenums="1" -lambda.MapHandler(([Event] BatchEvent batch, ILogger logger) => -{ - var processed = 0; - var failed = 0; - - foreach (var record in batch.Records) - { - if (record.DataContent is null) - { - logger.LogWarning("Failed to deserialize record {RecordId}", record.Id); - failed++; - continue; - } - - logger.LogInformation( - "Processing record {RecordId}: {Type} x{Count}", - record.Id, - record.DataContent.Type, - record.DataContent.Count - ); - - ProcessRecord(record.DataContent); - processed++; - } - - return new { Processed = processed, Failed = failed }; -}); -``` - ---- - -### Best Practices - -#### ✅ Do - -- **Check for null after extraction** - Deserialization can fail -- **Use `[JsonIgnore]` on content properties** - Prevents circular serialization -- **Handle exceptions gracefully** - Set content to `null` on deserialization errors -- **Use `EnvelopeOptions`** - Access configured JSON/XML settings through the `options` parameter -- **Log deserialization failures** - Helps with debugging malformed payloads - -#### ❌ Don't - -- **Don't assume `PayloadContent` is non-null** - Always check before using -- **Don't throw exceptions in `ExtractPayload`** - Handle errors gracefully -- **Don't modify the original payload string** - Only populate the deserialized content property - ---- - -### Key Concepts - -1. **Automatic Invocation** - The framework calls `ExtractPayload()` and `PackPayload()` automatically via middleware - -2. **Separation of Concerns** - Keep serialized strings and deserialized objects separate using `[JsonIgnore]` - -3. **Configuration Access** - Use `options.JsonOptions`, `options.XmlReaderSettings`, etc. for consistent serialization settings - -4. **Error Handling** - Set content properties to `null` when deserialization fails rather than throwing exceptions - ---- - -## Choosing the Right Feature - -### When to Use Envelopes - -Use envelope packages when: - -- ✅ You need type-safe access to message payloads from AWS event sources -- ✅ You want compile-time type checking for event data -- ✅ You're tired of manually parsing JSON from event bodies -- ✅ You need custom serialization formats (XML, Protobuf, etc.) -- ✅ You want IDE IntelliSense support for message structures - ---- - -## Installation - -### Envelope Packages - -Install only the envelope packages you need: - -```bash -# SQS envelope -dotnet add package AwsLambda.Host.Envelopes.Sqs - -# API Gateway envelope -dotnet add package AwsLambda.Host.Envelopes.ApiGateway - -# Other envelopes... -``` +Not every Lambda needs an envelope—handlers that already consume strongly typed SDK models can rely +on the base event types. Mix and match envelopes based on the triggers your project uses. From 85de927fa43a453cfca23d996acb32388b8a360b Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 16:09:44 -0500 Subject: [PATCH 59/65] feat(docs): enhance index and guides with improved content and structure - Updated `index.md` with a clearer description of `AwsLambda.Host`, replacing outdated definitions. - Introduced new content under `Hosting & Builder` to guide advanced customization and troubleshooting. - Improved clarity and formatting in the `Examples` section, marking incomplete areas as "Coming Soon." - Streamlined `Why AwsLambda.Host` and feature highlights with refined language for better readability. - Enhanced table of contents in `mkdocs.yml` with the addition of `Hosting & Builder` and font updates. - Added `JetBrains Mono` font for improved readability of code snippets. - Enabled plugin for optimization while suppressing unnecessary print summaries. --- docs/guides/index.md | 11 ++++++ docs/index.md | 89 ++++++++++++++++++-------------------------- mkdocs.yml | 5 +++ 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/docs/guides/index.md b/docs/guides/index.md index 2cdf9b24..958c155b 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -51,6 +51,17 @@ Register type-safe Lambda handlers with automatic dependency injection and sourc - Source generation benefits - Handler patterns +### [Hosting & Builder](hosting.md) +Understand what `LambdaApplication.CreateBuilder()` configures, how the runtime composes middleware, +and how to customize the host for advanced scenarios. + +**Topics covered:** + +- Builder defaults (configuration, logging, DI) +- LambdaApplicationOptions customization +- LambdaHostedService orchestration +- Default serializers and cancellation factories +- Troubleshooting host setup ### [Configuration](configuration.md) Configure framework behavior with LambdaHostOptions and application settings. diff --git a/docs/index.md b/docs/index.md index 5d492217..5e1f9d6e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,17 +5,22 @@ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=j-d-ha_aws-lambda-host&metric=alert_status&token=9fb519975d91379dcfbc6c13a4bd4207131af6e3)](https://sonarcloud.io/summary/new_code?id=j-d-ha_aws-lambda-host) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/j-d-ha/aws-lambda-host/blob/main/LICENSE) -A modern .NET framework that brings familiar .NET Core patterns to AWS Lambda - middleware, -dependency injection, and lifecycle hooks. +AwsLambda.Host is a lightweight hosting framework for .NET developers who want the comfort of ASP.NET +Core’s middleware, dependency injection, and lifecycle hooks inside AWS Lambda. Instead of wiring up +scopes, serializers, and cancellation tokens by hand, you map strongly typed handlers and let the +source-generated pipeline handle the plumbing. The result: less boilerplate, better testability, and +handlers that feel like the rest of your .NET codebase. [Get Started](getting-started/){ .md-button .md-button--primary } -[View Examples](examples/){ .md-button } +[Guides](guides/){ .md-button } +[Examples (Coming Soon)](examples/){ .md-button } --- ## Why AwsLambda.Host? -Stop writing boilerplate Lambda code. Start building features with patterns you already know. +Stop wiring up DI scopes, serializers, and cancellation tokens by hand. Ship features with patterns you +already know, while still embracing Lambda’s execution model. === "Traditional Lambda" @@ -106,48 +111,47 @@ Stop writing boilerplate Lambda code. Start building features with patterns you ### :material-view-dashboard-outline: .NET Hosting Patterns -Use middleware, builder pattern, and dependency injection similar to ASP.NET Core, with proper +Use middleware, the builder pattern, and dependency injection just like ASP.NET Core—with proper scoped lifetime management per invocation. [Learn about DI](guides/dependency-injection.md){ .md-button } ### :material-calendar-sync-outline: Lifecycle Management -Native support for async/await with proper Lambda timeout and cancellation handling built-in. +Run OnInit/OnShutdown hooks alongside your handler pipeline to warm resources, clear Lambda log +formatting, and flush telemetry with host-managed cancellation tokens. -[See lifecycle management](guides/lifecycle-management.md){ .md-button } +[Lifecycle management](guides/lifecycle-management.md){ .md-button } -### :material-code-braces: Source Generators & Interceptors +### :material-code-braces: Source-Generated Handlers -Compile-time code generation and method interception for optimal performance with zero runtime -reflection. +Compile-time interception validates handler signatures, injects dependencies, and avoids reflection. -[Explore advanced topics](advanced/source-generators.md){ .md-button } +[Handler registration](guides/handler-registration.md){ .md-button } -### :material-rocket-launch-outline: AOT Ready +### :material-rocket-launch-outline: AOT Friendly -Full support for Ahead-of-Time compilation for faster cold starts and reduced memory footprint. +Source generation plus System.Text.Json contexts keep handlers ready for Native AOT publishing. -[AOT compilation guide](advanced/aot-compilation.md){ .md-button } +[Advanced topics (Coming Soon)](advanced/index.md){ .md-button } ### :material-chart-line: Built-in Observability -OpenTelemetry integration for distributed tracing with automatic root span creation and custom -instrumentation. +OpenTelemetry integration emits traces and metrics without bolting on custom shims. [OpenTelemetry setup](features/opentelemetry.md){ .md-button } ### :material-code-json: Flexible Handler Registration -Simple, declarative API for mapping Lambda event types to handlers with compile-time type safety. +Map strongly typed handlers to envelopes or raw events with compile-time validation. [Handler registration](guides/handler-registration.md){ .md-button } ### :material-speedometer: Minimal Runtime Overhead -No unnecessary abstractions - efficient use of Lambda resources with optimized execution paths. +Small abstraction surface area keeps CPU and memory usage predictable inside Lambda’s sandbox. -[Performance optimization](advanced/performance-optimization.md){ .md-button } +[Advanced topics (Coming Soon)](advanced/index.md){ .md-button } --- @@ -163,20 +167,13 @@ Create your first Lambda handler: ```csharp using AwsLambda.Host.Builder; -using Microsoft.Extensions.Hosting; -// Create the application builder var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); -// Build the Lambda application -await using var lambda = builder.Build(); - -// Map your handler - the event is automatically injected lambda.MapHandler(([Event] string name) => $"Hello {name}!"); -// Run the Lambda await lambda.RunAsync(); - ``` !!! tip "Next Steps" @@ -227,16 +224,10 @@ deserialization. ## Examples & Use Cases -Explore complete example projects demonstrating real-world Lambda patterns: +Explore the repository’s `examples/` folder and the docs’ [Examples](examples/) page (content coming +soon) for end-to-end Lambda samples that wire up middleware, envelopes, and DI. -- **[Hello World](examples/hello-world.md)** - Basic Lambda with dependency injection and middleware -- **[REST API](examples/api-rest.md)** - API Gateway integration with request/response handling -- **[SQS Processing](examples/sqs-processing.md)** - Event-driven message processing -- **[OpenTelemetry](examples/opentelemetry-example.md)** - Full observability with distributed - tracing -- **[AOT Compilation](examples/aot-example.md)** - Native AOT for optimal cold start performance - -[View all examples](examples/){ .md-button } +[Examples (Coming Soon)](examples/){ .md-button } --- @@ -244,28 +235,22 @@ Explore complete example projects demonstrating real-world Lambda patterns: ### Get Involved -- **[GitHub Repository](https://github.com/j-d-ha/aws-lambda-host)** - Source code, issues, and - discussions -- **[Changelog](changelog.md)** - Version history and release notes -- **[License](https://github.com/j-d-ha/aws-lambda-host/blob/main/LICENSE)** - MIT License +- **[GitHub Repository](https://github.com/j-d-ha/aws-lambda-host)** – Source code, issues, and discussions. +- **[Changelog](changelog.md)** – Version history and release notes. +- **[License](https://github.com/j-d-ha/aws-lambda-host/blob/main/LICENSE)** – MIT License. ### Documentation -- **[Getting Started](getting-started/)** - Installation and first Lambda tutorial -- **[Guides](guides/)** - Comprehensive feature documentation -- **[Features](features/)** - Envelopes and OpenTelemetry integration -- **[API Reference](api-reference/)** - Detailed API documentation -- **[Advanced Topics](advanced/)** - AOT, source generators, and performance +- **[Getting Started](getting-started/)** – Installation and first Lambda tutorial. +- **[Guides](guides/)** – In-depth docs on DI, middleware, lifecycle, configuration, and more. +- **[Features](features/)** – Envelopes, OpenTelemetry integration, and other add-ons. +- **[Advanced Topics](advanced/)** – Coming soon: AOT, source generators, performance tuning. ### Support -Need help or want to contribute? - -- Browse the [FAQ](resources/faq.md) for common questions -- Check the [Troubleshooting Guide](resources/troubleshooting.md) for solutions -- Visit the [Community Page](resources/community.md) for support channels +- Ask or search in [GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions). +- File bugs or feature requests via [GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues). --- -**Ready to modernize your Lambda development?** [Get started now](getting-started/){ .md-button -.md-button--primary } +**Ready to modernize your Lambda development?** [Get started now](getting-started/){ .md-button .md-button--primary } diff --git a/mkdocs.yml b/mkdocs.yml index ad71b368..42f49ef3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,8 @@ theme: toggle: icon: material/brightness-4 name: Switch to light mode + font: + code: JetBrains Mono markdown_extensions: # Python Markdown extensions @@ -96,6 +98,7 @@ nav: - Middleware: guides/middleware.md - Lifecycle Management: guides/lifecycle-management.md - Handler Registration: guides/handler-registration.md + - Hosting & Builder: guides/hosting.md - Configuration: guides/configuration.md - Error Handling: guides/error-handling.md - Testing: guides/testing.md @@ -110,6 +113,8 @@ nav: plugins: - search + - optimize: + print_gain_summary: false extra_css: - stylesheets/extra.css From edbf076491a0ad641f9be45d3aef071c834a1d11 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 17:49:41 -0500 Subject: [PATCH 60/65] feat(docs): restructure and enhance getting started guide - Consolidated event handling and observability sections under "Extend the Framework." - Updated links and descriptions for envelopes and OpenTelemetry integration. - Improved clarity and focus by removing redundant subsections for performance optimization. - Adjusted .gitignore to include a new `.cache/` entry. --- .gitignore | 1 + docs/getting-started/first-lambda.md | 16 +++------------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 038dd053..9bf17d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ nunit-*.xml /temp.md /.claude/ /site/ +/.cache/ diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md index 988f03d7..f114e7da 100644 --- a/docs/getting-started/first-lambda.md +++ b/docs/getting-started/first-lambda.md @@ -617,17 +617,7 @@ Now that you have a working Lambda function, dive deeper into the framework: - **[Testing Strategies](/guides/testing.md)** – Unit and integration testing - **[Deployment Best Practices](/guides/deployment.md)** – CI/CD and production deployments -### Add Type-Safe Event Handling +### Extend the Framework -- **[SQS Envelope](/features/envelopes/sqs.md)** – Process SQS messages -- **[API Gateway Envelope](/features/envelopes/api-gateway.md)** – Build HTTP APIs -- **[SNS Envelope](/features/envelopes/sns.md)** – Handle SNS notifications - -### Enhance Observability - -- **[OpenTelemetry Integration](/features/opentelemetry.md)** – Add distributed tracing and metrics - -### Optimize Performance - -- **[AOT Compilation](/advanced/aot-compilation.md)** – Achieve fastest cold starts -- **[Performance Optimization](/advanced/performance-optimization.md)** – Profiling and tuning +- **[Envelopes](/features/envelopes.md)** – Type-safe event handling for SQS, SNS, API Gateway, and more. +- **[OpenTelemetry Integration](/features/open_telemetry.md)** – Add distributed tracing and metrics. From dc1ccf3e54fb99ccae159ff3746fd4c86ea82e59 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 17:54:51 -0500 Subject: [PATCH 61/65] feat(docs): update relative links for consistency and maintainability - Replaced absolute paths with relative links across all documentation files. - Updated references in `getting-started`, `guides`, and `features` sections for improved portability. - Removed outdated sections such as `Troubleshooting` and `FAQ` to streamline documentation. - Enhanced link consistency in package tables and examples for improved navigation. - Fixed broken URLs and updated links to GitHub examples for clarity. --- docs/features/open_telemetry.md | 2 +- docs/getting-started/first-lambda.md | 11 +++--- docs/getting-started/index.md | 14 +++----- docs/getting-started/installation.md | 8 ++--- docs/guides/index.md | 10 +++--- docs/index.md | 52 ++++++++++++++-------------- 6 files changed, 44 insertions(+), 53 deletions(-) diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md index 7bde444e..f4bbad3a 100644 --- a/docs/features/open_telemetry.md +++ b/docs/features/open_telemetry.md @@ -186,7 +186,7 @@ All three methods also accept an optional `timeoutMilliseconds` parameter. This !!! note This code is not specific to `AwsLambda.Host.OpenTelemetry` and follows the guidlines provided by Microsoft's [.NET distributed tracing documetation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). -A full working example of an instrumented Lambda application can be found in [here](../../examples/AwsLambda.Host.Example.OpenTelemetry) +A full working example of an instrumented Lambda application can be found [on GitHub](https://github.com/j-d-ha/aws-lambda-host/tree/main/examples/AwsLambda.Host.Example.OpenTelemetry). ### Custom Instrumentation Class diff --git a/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md index f114e7da..0bccdfdc 100644 --- a/docs/getting-started/first-lambda.md +++ b/docs/getting-started/first-lambda.md @@ -612,12 +612,11 @@ Now that you have a working Lambda function, dive deeper into the framework: ### Explore Advanced Features -- **[Middleware Patterns](/guides/middleware.md)** – Build reusable middleware components -- **[Handler Registration](/guides/handler-registration.md)** – Advanced handler patterns -- **[Testing Strategies](/guides/testing.md)** – Unit and integration testing -- **[Deployment Best Practices](/guides/deployment.md)** – CI/CD and production deployments +- **[Middleware Patterns](../guides/middleware.md)** – Build reusable middleware components +- **[Handler Registration](../guides/handler-registration.md)** – Advanced handler patterns +- **[Testing Strategies](../guides/testing.md)** – Unit and integration testing ### Extend the Framework -- **[Envelopes](/features/envelopes.md)** – Type-safe event handling for SQS, SNS, API Gateway, and more. -- **[OpenTelemetry Integration](/features/open_telemetry.md)** – Add distributed tracing and metrics. +- **[Envelopes](../features/envelopes.md)** – Type-safe event handling for SQS, SNS, API Gateway, and more. +- **[OpenTelemetry Integration](../features/open_telemetry.md)** – Add distributed tracing and metrics. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index d1dbee47..dc979945 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -29,7 +29,7 @@ Before you begin, ensure you have: - **[Your First Lambda](first-lambda.md)** – Walk through a handler, DI setup, and local testing. - **[Core Concepts](core-concepts.md)** – Learn about the host lifecycle, middleware, and source generation. -Prefer to explore? Head directly to **[Guides](/guides/)**, **[Examples](/examples/)**, or the **[API Reference](/api-reference/)** for deeper dives. +Prefer to explore? Head directly to **[Guides](../guides/index.md)** or **[Examples](../examples/index.md)** for deeper dives. ## Framework Highlights @@ -40,19 +40,15 @@ Prefer to explore? Head directly to **[Guides](/guides/)**, **[Examples](/exampl ## Explore Features -- **[Envelopes](/features/envelopes/)** – Type-safe event source integration (SQS, SNS, API Gateway, etc.) -- **[OpenTelemetry](/features/open_telemetry.md)** – Distributed tracing and observability -- **[AOT Compilation](/advanced/aot-compilation.md)** – Optimize for fastest cold starts -- **[Source Generators](/advanced/source-generators.md)** – Understand compile-time optimizations - -## Need Help +- **[Envelopes](../features/envelopes.md)** – Type-safe event source integration (SQS, SNS, API Gateway, etc.) +- **[OpenTelemetry](../features/open_telemetry.md)** – Distributed tracing and observability +- **[AOT Compilation](../advanced/index.md)** – Optimize for fastest cold starts (coming soon) +- **[Source Generators](../advanced/index.md)** – Understand compile-time optimizations (coming soon) ## Getting Help If you run into issues or have questions: -- **[FAQ](/resources/faq.md)** – Common questions and answers -- **[Troubleshooting](/resources/troubleshooting.md)** – Solutions to common problems - **[GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues)** – Report bugs or request features - **[GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions)** – Ask questions and share ideas diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 505c5dce..ef6fb78f 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -200,7 +200,7 @@ Envelope packages provide type-safe, strongly-typed event handling for specific | **AwsLambda.Host.Envelopes.Alb** | Application Load Balancer | ALB target Lambda functions | !!! info "Envelope Packages" - You only need envelope packages if you're working with those specific event sources. For simple use cases, just `AwsLambda.Host` is sufficient. Learn more in the [Envelopes documentation](/features/envelopes/). + You only need envelope packages if you're working with those specific event sources. For simple use cases, just `AwsLambda.Host` is sufficient. Learn more in the [Envelopes documentation](../features/envelopes.md). ## Troubleshooting @@ -231,8 +231,6 @@ Envelope packages provide type-safe, strongly-typed event handling for specific If you encounter issues not covered here: -- Check the [Troubleshooting Guide](/resources/troubleshooting.md) -- Review [Common Questions](/resources/faq.md) - Search or ask in [GitHub Discussions](https://github.com/j-d-ha/aws-lambda-host/discussions) - Report bugs in [GitHub Issues](https://github.com/j-d-ha/aws-lambda-host/issues) @@ -245,6 +243,4 @@ Now that you have `AwsLambda.Host` installed and verified, you're ready to build ### Additional Resources - [Core Concepts](core-concepts.md) – Understand the framework architecture -- [Project Structure](project-structure.md) – Learn how to organize your code -- [Examples](/examples/) – Browse complete working examples -- [API Reference](/api-reference/) – Detailed API documentation +- [Examples](../examples/index.md) – Browse complete working examples diff --git a/docs/guides/index.md b/docs/guides/index.md index 958c155b..b0899c23 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -106,7 +106,7 @@ Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, an ### New to `AwsLambda.Host`? -Start with [Getting Started](/getting-started/) to build your first Lambda function, then return here for deeper coverage of specific features. +Start with [Getting Started](../getting-started/index.md) to build your first Lambda function, then return here for deeper coverage of specific features. ### Building Production Lambda Functions? @@ -114,13 +114,13 @@ Use these guides as reference documentation when implementing specific features. ### Optimizing Performance? -After mastering the guides, explore [Advanced Topics](/advanced/) for AOT compilation, source generators, and performance optimization. +After mastering the guides, explore [Advanced Topics](../advanced/index.md) for AOT compilation, source generators, and performance optimization. ## Additional Resources -- **[Examples (Coming Soon)](/examples/)** – Guided sample apps covering middleware, envelopes, and DI wiring. -- **[Features](/features/)** – Envelope packages and OpenTelemetry add-ons. -- **[Advanced Topics (Coming Soon)](/advanced/)** – Native AOT, generator internals, and performance deep dives. +- **[Examples (Coming Soon)](../examples/index.md)** – Guided sample apps covering middleware, envelopes, and DI wiring. +- **[Features](../features/index.md)** – Envelope packages and OpenTelemetry add-ons. +- **[Advanced Topics (Coming Soon)](../advanced/index.md)** – Native AOT, generator internals, and performance deep dives. ## Getting Help diff --git a/docs/index.md b/docs/index.md index 5e1f9d6e..1b1a0a15 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,9 +11,9 @@ scopes, serializers, and cancellation tokens by hand, you map strongly typed han source-generated pipeline handle the plumbing. The result: less boilerplate, better testability, and handlers that feel like the rest of your .NET codebase. -[Get Started](getting-started/){ .md-button .md-button--primary } -[Guides](guides/){ .md-button } -[Examples (Coming Soon)](examples/){ .md-button } +[Get Started](getting-started/index.md){ .md-button .md-button--primary } +[Guides](guides/index.md){ .md-button } +[Examples (Coming Soon)](examples/index.md){ .md-button } --- @@ -139,7 +139,7 @@ Source generation plus System.Text.Json contexts keep handlers ready for Native OpenTelemetry integration emits traces and metrics without bolting on custom shims. -[OpenTelemetry setup](features/opentelemetry.md){ .md-button } +[OpenTelemetry setup](features/open_telemetry.md){ .md-button } ### :material-code-json: Flexible Handler Registration @@ -177,8 +177,8 @@ await lambda.RunAsync(); ``` !!! tip "Next Steps" - Ready to dive deeper? Check out the [Getting Started Guide](getting-started/) for a complete - tutorial, or explore the [Examples](examples/) to see real-world applications. + Ready to dive deeper? Check out the [Getting Started Guide](getting-started/index.md) for a complete + tutorial, or explore the [Examples](examples/index.md) to see real-world applications. --- @@ -191,9 +191,9 @@ for building AWS Lambda functions. | Package | Description | NuGet | Downloads | |------------------------------------------------------------------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| -| [**AwsLambda.Host**](api-reference/host.md) | Core hosting framework with middleware and DI | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host/) | -| [**AwsLambda.Host.Abstractions**](api-reference/abstractions.md) | Core interfaces and contracts | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions/) | -| [**AwsLambda.Host.OpenTelemetry**](features/opentelemetry.md) | Distributed tracing and observability | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry/) | +| [**AwsLambda.Host**](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/AwsLambda.Host) | Core hosting framework with middleware and DI | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.svg)](https://www.nuget.org/packages/AwsLambda.Host/) | +| [**AwsLambda.Host.Abstractions**](https://github.com/j-d-ha/aws-lambda-host/tree/main/src/AwsLambda.Host.Abstractions) | Core interfaces and contracts | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Abstractions.svg)](https://www.nuget.org/packages/AwsLambda.Host.Abstractions/) | +| [**AwsLambda.Host.OpenTelemetry**](features/open_telemetry.md) | Distributed tracing and observability | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.OpenTelemetry.svg)](https://www.nuget.org/packages/AwsLambda.Host.OpenTelemetry/) | ### Envelope Packages @@ -205,29 +205,29 @@ deserialization. safety and automatic deserialization of message bodies from SQS, SNS, Kinesis, and other event sources. - [Learn more about envelopes](features/envelopes/){ .md-button } + [Learn more about envelopes](features/envelopes.md){ .md-button } | Package | Description | NuGet | Downloads | |----------------------------------------------------------------------------------------|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [**AwsLambda.Host.Envelopes.Sqs**](features/envelopes/sqs.md) | Simple Queue Service events with typed message bodies | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs/) | -| [**AwsLambda.Host.Envelopes.Sns**](features/envelopes/sns.md) | Simple Notification Service messages | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns/) | -| [**AwsLambda.Host.Envelopes.ApiGateway**](features/envelopes/api-gateway.md) | REST, HTTP, and WebSocket APIs | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway/) | -| [**AwsLambda.Host.Envelopes.Kinesis**](features/envelopes/kinesis.md) | Data Streams with typed records | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis/) | -| [**AwsLambda.Host.Envelopes.KinesisFirehose**](features/envelopes/kinesis-firehose.md) | Data transformation | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose/) | -| [**AwsLambda.Host.Envelopes.Kafka**](features/envelopes/kafka.md) | MSK and self-managed Kafka | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka/) | -| [**AwsLambda.Host.Envelopes.CloudWatchLogs**](features/envelopes/cloudwatch-logs.md) | Log subscriptions | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs/) | -| [**AwsLambda.Host.Envelopes.Alb**](features/envelopes/alb.md) | Application Load Balancer requests | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb/) | +| **AwsLambda.Host.Envelopes.Sqs** | Simple Queue Service events with typed message bodies | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sqs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sqs/) | +| **AwsLambda.Host.Envelopes.Sns** | Simple Notification Service messages | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Sns.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Sns/) | +| **AwsLambda.Host.Envelopes.ApiGateway** | REST, HTTP, and WebSocket APIs | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.ApiGateway/) | +| **AwsLambda.Host.Envelopes.Kinesis** | Data Streams with typed records | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kinesis/) | +| **AwsLambda.Host.Envelopes.KinesisFirehose** | Data transformation | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.KinesisFirehose/) | +| **AwsLambda.Host.Envelopes.Kafka** | MSK and self-managed Kafka | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Kafka.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Kafka/) | +| **AwsLambda.Host.Envelopes.CloudWatchLogs** | Log subscriptions | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.CloudWatchLogs/) | +| **AwsLambda.Host.Envelopes.Alb** | Application Load Balancer requests | [![NuGet](https://img.shields.io/nuget/v/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb) | [![Downloads](https://img.shields.io/nuget/dt/AwsLambda.Host.Envelopes.Alb.svg)](https://www.nuget.org/packages/AwsLambda.Host.Envelopes.Alb/) | -[Browse all envelope packages](features/envelopes/){ .md-button } +[Browse all envelope packages](features/envelopes.md){ .md-button } --- ## Examples & Use Cases -Explore the repository’s `examples/` folder and the docs’ [Examples](examples/) page (content coming +Explore the repository’s `examples/` folder and the docs’ [Examples](examples/index.md) page (content coming soon) for end-to-end Lambda samples that wire up middleware, envelopes, and DI. -[Examples (Coming Soon)](examples/){ .md-button } +[Examples (Coming Soon)](examples/index.md){ .md-button } --- @@ -241,10 +241,10 @@ soon) for end-to-end Lambda samples that wire up middleware, envelopes, and DI. ### Documentation -- **[Getting Started](getting-started/)** – Installation and first Lambda tutorial. -- **[Guides](guides/)** – In-depth docs on DI, middleware, lifecycle, configuration, and more. -- **[Features](features/)** – Envelopes, OpenTelemetry integration, and other add-ons. -- **[Advanced Topics](advanced/)** – Coming soon: AOT, source generators, performance tuning. +- **[Getting Started](getting-started/index.md)** – Installation and first Lambda tutorial. +- **[Guides](guides/index.md)** – In-depth docs on DI, middleware, lifecycle, configuration, and more. +- **[Features](features/index.md)** – Envelopes, OpenTelemetry integration, and other add-ons. +- **[Advanced Topics](advanced/index.md)** – Coming soon: AOT, source generators, performance tuning. ### Support @@ -253,4 +253,4 @@ soon) for end-to-end Lambda samples that wire up middleware, envelopes, and DI. --- -**Ready to modernize your Lambda development?** [Get started now](getting-started/){ .md-button .md-button--primary } +**Ready to modernize your Lambda development?** [Get started now](getting-started/index.md){ .md-button .md-button--primary } From b205c6ba85d283527d4fde7827e046c7e5283613 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 18:43:01 -0500 Subject: [PATCH 62/65] feat(docs): add links to full documentation across all README files - Added a direct link to the full documentation in all package and example README files. - Enhanced navigation and accessibility for users to access detailed guides and resources. --- README.md | 2 ++ examples/AwsLambda.Host.Example.OpenTelemetry/README.md | 2 ++ src/AwsLambda.Host.Abstractions/README.md | 2 ++ src/AwsLambda.Host.OpenTelemetry/README.md | 2 ++ src/AwsLambda.Host/README.md | 2 ++ src/Envelopes/README.md | 2 ++ 6 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 2575eb06..f33a9b43 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=j-d-ha_aws-lambda-host&metric=alert_status&token=9fb519975d91379dcfbc6c13a4bd4207131af6e3)](https://sonarcloud.io/summary/new_code?id=j-d-ha_aws-lambda-host) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ + A modern .NET framework for building AWS Lambda functions using familiar .NET patterns. ## Overview diff --git a/examples/AwsLambda.Host.Example.OpenTelemetry/README.md b/examples/AwsLambda.Host.Example.OpenTelemetry/README.md index f08f1674..7f88cceb 100644 --- a/examples/AwsLambda.Host.Example.OpenTelemetry/README.md +++ b/examples/AwsLambda.Host.Example.OpenTelemetry/README.md @@ -1,5 +1,7 @@ # AwsLambda.Host.Example.OpenTelemetry +> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ + ## Getting Started You may need to CD into this directory before running the following commands. diff --git a/src/AwsLambda.Host.Abstractions/README.md b/src/AwsLambda.Host.Abstractions/README.md index 18865410..5672569a 100644 --- a/src/AwsLambda.Host.Abstractions/README.md +++ b/src/AwsLambda.Host.Abstractions/README.md @@ -2,6 +2,8 @@ Core interfaces and abstractions for the aws-lambda-host framework. +> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ + ## Overview Core interfaces and delegates that define the AwsLambda.Host framework contract. This package diff --git a/src/AwsLambda.Host.OpenTelemetry/README.md b/src/AwsLambda.Host.OpenTelemetry/README.md index dbc4e233..7cbe5849 100644 --- a/src/AwsLambda.Host.OpenTelemetry/README.md +++ b/src/AwsLambda.Host.OpenTelemetry/README.md @@ -2,6 +2,8 @@ OpenTelemetry integration for distributed tracing and observability in AWS Lambda functions. +> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ + ## Overview An extension package for the [AwsLambda.Host](../AwsLambda.Host/README.md) framework that provides diff --git a/src/AwsLambda.Host/README.md b/src/AwsLambda.Host/README.md index fce1849b..a0c2d31d 100644 --- a/src/AwsLambda.Host/README.md +++ b/src/AwsLambda.Host/README.md @@ -3,6 +3,8 @@ Core framework for building AWS Lambda functions with dependency injection, middleware, and source generation. +> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ + ## Overview A modern .NET framework for building AWS Lambda functions using familiar ASP.NET Core patterns. The diff --git a/src/Envelopes/README.md b/src/Envelopes/README.md index 874a786e..344fe235 100644 --- a/src/Envelopes/README.md +++ b/src/Envelopes/README.md @@ -3,6 +3,8 @@ Envelopes extend AWS Lambda event types with strongly-typed payload handling, making it easier to work with JSON (or other formats) in Lambda function handlers. +> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ + ## Overview Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) From 10028182918c3c8089deb0c3b32a55476ff475b8 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 18:48:54 -0500 Subject: [PATCH 63/65] fix(docs): update documentation links to improve consistency and formatting - Updated all README files to use consistent `[View Full Documentation]` link formatting. - Replaced the outdated inline text links with a standardized clickable markdown format. - Enhanced readability and navigation for users across all documentation. --- README.md | 6 +++--- examples/AwsLambda.Host.Example.OpenTelemetry/README.md | 2 +- src/AwsLambda.Host.Abstractions/README.md | 4 ++-- src/AwsLambda.Host.OpenTelemetry/README.md | 4 ++-- src/AwsLambda.Host/README.md | 4 ++-- src/Envelopes/README.md | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f33a9b43..fe18606b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ -# aws-lambda-host +# AwsLambda.Host [![Main Build](https://github.com/j-d-ha/aws-lambda-host/actions/workflows/main-build.yaml/badge.svg)](https://github.com/j-d-ha/aws-lambda-host/actions/workflows/main-build.yaml) [![codecov](https://codecov.io/gh/j-d-ha/aws-lambda-host/graph/badge.svg?token=BWORPTQ0UK)](https://codecov.io/gh/j-d-ha/aws-lambda-host) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=j-d-ha_aws-lambda-host&metric=alert_status&token=9fb519975d91379dcfbc6c13a4bd4207131af6e3)](https://sonarcloud.io/summary/new_code?id=j-d-ha_aws-lambda-host) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ - A modern .NET framework for building AWS Lambda functions using familiar .NET patterns. ## Overview @@ -18,6 +16,8 @@ with dependency injection (similar to minimal APIs), middleware support, and mod patterns. Built on the generic host from Microsoft.Extensions, it provides a .NET hosting experience similar to ASP.NET Core but tailored specifically for Lambda. +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Key Features - **.NET Hosting Patterns** – Use middleware, builder pattern, and dependency injection (similar to diff --git a/examples/AwsLambda.Host.Example.OpenTelemetry/README.md b/examples/AwsLambda.Host.Example.OpenTelemetry/README.md index 7f88cceb..4f4fd119 100644 --- a/examples/AwsLambda.Host.Example.OpenTelemetry/README.md +++ b/examples/AwsLambda.Host.Example.OpenTelemetry/README.md @@ -1,6 +1,6 @@ # AwsLambda.Host.Example.OpenTelemetry -> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** ## Getting Started diff --git a/src/AwsLambda.Host.Abstractions/README.md b/src/AwsLambda.Host.Abstractions/README.md index 5672569a..2eab60ee 100644 --- a/src/AwsLambda.Host.Abstractions/README.md +++ b/src/AwsLambda.Host.Abstractions/README.md @@ -2,8 +2,6 @@ Core interfaces and abstractions for the aws-lambda-host framework. -> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ - ## Overview Core interfaces and delegates that define the AwsLambda.Host framework contract. This package @@ -20,6 +18,8 @@ This package is typically used implicitly by [AwsLambda.Host](../AwsLambda.Host/ essential if you're building custom integrations, middleware components, or extensions to the framework. +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Installation Install via NuGet: diff --git a/src/AwsLambda.Host.OpenTelemetry/README.md b/src/AwsLambda.Host.OpenTelemetry/README.md index 7cbe5849..b4d940fd 100644 --- a/src/AwsLambda.Host.OpenTelemetry/README.md +++ b/src/AwsLambda.Host.OpenTelemetry/README.md @@ -2,8 +2,6 @@ OpenTelemetry integration for distributed tracing and observability in AWS Lambda functions. -> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ - ## Overview An extension package for the [AwsLambda.Host](../AwsLambda.Host/README.md) framework that provides @@ -24,6 +22,8 @@ comprehensive observability integration. This package enables: > standalone. Configure exporters to send traces and metrics to your observability backend (e.g., > Datadog, New Relic, Jaeger, CloudWatch). +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Installation **This package requires [AwsLambda.Host](../AwsLambda.Host/README.md) to be installed and working diff --git a/src/AwsLambda.Host/README.md b/src/AwsLambda.Host/README.md index a0c2d31d..4fb736ac 100644 --- a/src/AwsLambda.Host/README.md +++ b/src/AwsLambda.Host/README.md @@ -3,8 +3,6 @@ Core framework for building AWS Lambda functions with dependency injection, middleware, and source generation. -> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ - ## Overview A modern .NET framework for building AWS Lambda functions using familiar ASP.NET Core patterns. The @@ -19,6 +17,8 @@ core runtime provides: - **Lambda-Optimized Design**: Event handling, cold start reduction, and efficient resource utilization tailored to AWS Lambda constraints +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Installation Install the NuGet package: diff --git a/src/Envelopes/README.md b/src/Envelopes/README.md index 344fe235..97fa6950 100644 --- a/src/Envelopes/README.md +++ b/src/Envelopes/README.md @@ -3,8 +3,6 @@ Envelopes extend AWS Lambda event types with strongly-typed payload handling, making it easier to work with JSON (or other formats) in Lambda function handlers. -> 📚 **Full Documentation:** https://j-d-ha.github.io/aws-lambda-host/ - ## Overview Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) @@ -20,6 +18,8 @@ IDE support and compile-time type checking. - **AOT Ready** Support for Native AOT compilation via JsonSerializerContext registration - **Familiar API** Works seamlessly with existing AWS Lambda event patterns +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Packages | Lambda Event Type | Package | NuGet | Downloads | From fd8549d67589aea9fa203c9310d814e6c398794c Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 18:51:53 -0500 Subject: [PATCH 64/65] docs(readme): update documentation links for improved visibility - Standardized placement of `[View Full Documentation]` links across all README files. - Enhanced readability by moving duplicate links to a more prominent and consistent position. - Improved navigation for users accessing detailed documentation resources. --- README.md | 4 ++-- src/AwsLambda.Host.Abstractions/README.md | 4 ++-- src/AwsLambda.Host.OpenTelemetry/README.md | 4 ++-- src/AwsLambda.Host/README.md | 4 ++-- src/Envelopes/README.md | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fe18606b..7feed6a4 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A modern .NET framework for building AWS Lambda functions using familiar .NET patterns. +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Overview **aws-lambda-host** is a .NET hosting framework that simplifies AWS Lambda development by following @@ -16,8 +18,6 @@ with dependency injection (similar to minimal APIs), middleware support, and mod patterns. Built on the generic host from Microsoft.Extensions, it provides a .NET hosting experience similar to ASP.NET Core but tailored specifically for Lambda. -> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** - ## Key Features - **.NET Hosting Patterns** – Use middleware, builder pattern, and dependency injection (similar to diff --git a/src/AwsLambda.Host.Abstractions/README.md b/src/AwsLambda.Host.Abstractions/README.md index 2eab60ee..34b5b227 100644 --- a/src/AwsLambda.Host.Abstractions/README.md +++ b/src/AwsLambda.Host.Abstractions/README.md @@ -2,6 +2,8 @@ Core interfaces and abstractions for the aws-lambda-host framework. +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Overview Core interfaces and delegates that define the AwsLambda.Host framework contract. This package @@ -18,8 +20,6 @@ This package is typically used implicitly by [AwsLambda.Host](../AwsLambda.Host/ essential if you're building custom integrations, middleware components, or extensions to the framework. -> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** - ## Installation Install via NuGet: diff --git a/src/AwsLambda.Host.OpenTelemetry/README.md b/src/AwsLambda.Host.OpenTelemetry/README.md index b4d940fd..6fad3b23 100644 --- a/src/AwsLambda.Host.OpenTelemetry/README.md +++ b/src/AwsLambda.Host.OpenTelemetry/README.md @@ -2,6 +2,8 @@ OpenTelemetry integration for distributed tracing and observability in AWS Lambda functions. +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Overview An extension package for the [AwsLambda.Host](../AwsLambda.Host/README.md) framework that provides @@ -22,8 +24,6 @@ comprehensive observability integration. This package enables: > standalone. Configure exporters to send traces and metrics to your observability backend (e.g., > Datadog, New Relic, Jaeger, CloudWatch). -> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** - ## Installation **This package requires [AwsLambda.Host](../AwsLambda.Host/README.md) to be installed and working diff --git a/src/AwsLambda.Host/README.md b/src/AwsLambda.Host/README.md index 4fb736ac..d41ef69a 100644 --- a/src/AwsLambda.Host/README.md +++ b/src/AwsLambda.Host/README.md @@ -3,6 +3,8 @@ Core framework for building AWS Lambda functions with dependency injection, middleware, and source generation. +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Overview A modern .NET framework for building AWS Lambda functions using familiar ASP.NET Core patterns. The @@ -17,8 +19,6 @@ core runtime provides: - **Lambda-Optimized Design**: Event handling, cold start reduction, and efficient resource utilization tailored to AWS Lambda constraints -> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** - ## Installation Install the NuGet package: diff --git a/src/Envelopes/README.md b/src/Envelopes/README.md index 97fa6950..046c5be0 100644 --- a/src/Envelopes/README.md +++ b/src/Envelopes/README.md @@ -3,6 +3,8 @@ Envelopes extend AWS Lambda event types with strongly-typed payload handling, making it easier to work with JSON (or other formats) in Lambda function handlers. +> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** + ## Overview Envelope packages wrap official AWS Lambda event classes (like `SQSEvent`, `APIGatewayProxyRequest`) @@ -18,8 +20,6 @@ IDE support and compile-time type checking. - **AOT Ready** Support for Native AOT compilation via JsonSerializerContext registration - **Familiar API** Works seamlessly with existing AWS Lambda event patterns -> 📚 **[View Full Documentation](https://j-d-ha.github.io/aws-lambda-host/)** - ## Packages | Lambda Event Type | Package | NuGet | Downloads | From f2dd3565549abfa4fa61402730e7512cc9fedd04 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 1 Dec 2025 18:55:08 -0500 Subject: [PATCH 65/65] feat(ci): add pngquant installation step to docs workflow - Included installation of pngquant in the GitHub Actions workflow for image optimization. - Ensures compatibility with tools requiring pngquant during documentation build processes. --- .github/workflows/docs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 42ae821d..bd3eb0e9 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -35,6 +35,9 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Install pngquant + run: sudo apt-get update && sudo apt-get install -y pngquant - name: Setup Python uses: actions/setup-python@v5