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 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/AwsLambda.Host.sln b/AwsLambda.Host.sln index 2e9c52a8..d0bf4f62 100644 --- a/AwsLambda.Host.sln +++ b/AwsLambda.Host.sln @@ -18,8 +18,8 @@ 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 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" 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 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 diff --git a/README.md b/README.md index 2575eb06..7feed6a4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 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) @@ -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 diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 00000000..9ec02e9e --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,5 @@ +# Advanced Topics + +> 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/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/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/features/envelopes.md b/docs/features/envelopes.md new file mode 100644 index 00000000..d44b9528 --- /dev/null +++ b/docs/features/envelopes.md @@ -0,0 +1,192 @@ +# 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. + +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. + +## Why Envelopes? + +- **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.). + +## Provided Envelopes + +Install only the envelopes you need; each one lives in its own NuGet package. + +| 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) | + +Each package ships with README examples in the repository if you need event-specific guidance. + +## Quick Start + +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 +dotnet add package AwsLambda.Host.Envelopes.Sqs +``` + +```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(); + +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(); // optional when you want to signal per-message failures + } +); + +await lambda.RunAsync(); + +internal sealed record OrderMessage(string OrderId, decimal Amount); +``` + +## Custom Serialization & EnvelopeOptions + +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; +using System.Text.Json.Serialization; + +builder.Services.ConfigureEnvelopeOptions(options => +{ + options.JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonOptions.TypeInfoResolver = MyEnvelopeJsonContext.Default; // Native AOT +}); +``` + +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 Configuration + +- **`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="Program.cs" linenums="1" + builder.Services.ConfigureEnvelopeOptions(options => + { + options.Items["SchemaVersion"] = "v2"; + options.Items["Validator"] = new PayloadValidator(); + }); + ``` + + Inside your envelope implementation, read `options.Items` to control validation or routing logic. + +## Creating Custom Envelopes + +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. + +```csharp title="CustomRequestEnvelope.cs" linenums="1" +using System.Text.Json; +using System.Text.Json.Serialization; +using AwsLambda.Host.Envelopes; +using AwsLambda.Host.Options; + +public sealed class CustomRequestEnvelope : IRequestEnvelope +{ + [JsonPropertyName("payload")] + public required string Payload { get; set; } + + [JsonIgnore] + public MyPayload? PayloadContent { get; private set; } + + public void ExtractPayload(EnvelopeOptions options) + { + PayloadContent = JsonSerializer.Deserialize(Payload, options.JsonOptions); + } +} +``` + +In a handler: + +```csharp title="Program.cs" linenums="1" +lambda.MapHandler(([Event] CustomRequestEnvelope envelope) => +{ + if (envelope.PayloadContent is null) + return new { Error = "Invalid payload" }; + + return new { Success = true, envelope.PayloadContent.Name }; +}); +``` + +Response envelopes work the same way—implement `IResponseEnvelope` and serialize into a string +property inside `PackPayload`. + +### Batch Envelopes + +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. + +## Best Practices + +- **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. + +## When to Use Envelopes + +Choose envelopes whenever: + +- 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. + +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. diff --git a/docs/features/index.md b/docs/features/index.md new file mode 100644 index 00000000..06dfbcb2 --- /dev/null +++ b/docs/features/index.md @@ -0,0 +1,18 @@ +# Features + +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. + +This section provides an overview of the available features. + +--- + +## Feature Categories + +### [Envelope Pattern](./envelopes.md) + +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. + +### [Observability (OpenTelemetry)](./open_telemetry.md) + +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. + diff --git a/docs/features/open_telemetry.md b/docs/features/open_telemetry.md new file mode 100644 index 00000000..f4bbad3a --- /dev/null +++ b/docs/features/open_telemetry.md @@ -0,0 +1,303 @@ +# OpenTelemetry + +## 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. + +At compile time, it wraps your handler invocation in a root trace span, providing reflection-free, high-performance distributed tracing. + +--- + +## Key Benefits + +- **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. +- **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 + +### Installation + +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.OpenTelemetryProtocol +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" +using AwsLambda.Host.Builder; +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(); + +builder + .Services.AddOpenTelemetry()// (1)! + .WithTracing(tracing => + { + tracing.AddAWSLambdaConfigurations();// (2)! + tracing.AddSource("MyService");// (3)! + tracing.SetResourceBuilder( + 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();// (7)! + +lambda.OnShutdownFlushOpenTelemetry();// (8)! + +lambda.MapHandler(// (9)! + async ([Event] Request request, ILogger logger, CancellationToken cancellationToken) => + { + logger.LogInformation("Responding to {Name}", request.Name); + + await Task.Delay(100, cancellationToken); // Simulate work + + return new Response($"Hello {request.Name}!"); + } +); + +await lambda.RunAsync(); + +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. 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). + + When working with `AwsLambda.Host`, its recommended to the latter approach and as such, this documentation focuses on it. + +--- + +## How It Works: Source Generation and Interceptors + +`AwsLambda.Host` relies on C# source generators to avoid runtime reflection and improve performance. The `UseOpenTelemetryTracing()` method is a key part of this system. + +Here's the step-by-step process: + +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. + +This entire process happens during compilation, resulting in highly optimized code that instruments your handler without any reflection overhead at runtime. + +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. + +--- + +## 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). + +`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 + +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 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. + +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. + +!!! 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 + 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). + +--- + +## 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 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). + +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 + +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; + +namespace AwsLambda.Host.Example.OpenTelemetry; + +/// +/// 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"; + + internal ActivitySource ActivitySource { get; } = + new(ActivitySourceName, ActivitySourceVersion); + + public void Dispose() => ActivitySource.Dispose(); +} +``` + +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; + +namespace AwsLambda.Host.Example.OpenTelemetry; + +public class NameMetrics +{ + private readonly Counter _namesProcessed; + + public NameMetrics(IMeterFactory meterFactory) + { + var meter = meterFactory.Create("MyLambda.Service"); + _namesProcessed = meter.CreateCounter("MyLambda.Service.Processed"); + } + + public void ProcessName(string name) => + _namesProcessed.Add(1, new KeyValuePair("name", name)); +} +``` + +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; + +namespace AwsLambda.Host.Example.OpenTelemetry; + +internal class NameService(Instrumentation instrumentation, NameMetrics nameMetrics) +{ + private readonly ActivitySource _activitySource = instrumentation.ActivitySource; + + public async Task GetFullName(string name, CancellationToken cancellationToken = default) + { + using var activity = _activitySource.StartActivity("GetFullName"); + activity?.SetTag("input.name", name); + + 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. 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 + +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; + +namespace AwsLambda.Host.Example.OpenTelemetry; + +internal static class Function +{ + internal static async Task Handler( + [Event] Request request, + IService service, + Instrumentation instrumentation, + CancellationToken cancellationToken + ) + { + using var activity = instrumentation.ActivitySource.StartActivity(); + + var message = await service.GetMessage(request.Name, cancellationToken); + + return new Response(message, DateTime.UtcNow); + } +} +``` + +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. diff --git a/docs/getting-started/core-concepts.md b/docs/getting-started/core-concepts.md new file mode 100644 index 00000000..ea3b47e9 --- /dev/null +++ b/docs/getting-started/core-concepts.md @@ -0,0 +1,264 @@ +# Core Concepts + +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. `AwsLambda.Host` exposes each one so you can opt into work at the right time. + +``` +Cold Start → OnInit → Invocation 1..N → OnShutdown → Termination +``` + +### OnInit (Cold Start) + +OnInit handlers run once when AWS boots a new execution environment. + +**Characteristics:** + +- 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. + +**Great for:** + +- 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. + +```csharp title="Program.cs" linenums="1" +lambda.OnInit(async ( + ICache cache, + ILogger logger, + CancellationToken ct +) => +{ + logger.LogInformation("Warming cache before first invocation"); + await cache.PopulateAsync(ct); + return true; // omit when you do not need to signal failure explicitly +}); +``` + +### Invocation Pipeline + +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. `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`. +7. The invocation scope is disposed. + +### OnShutdown (Teardown) + +AWS sends SIGTERM before reclaiming the execution environment. OnShutdown handlers allow you to flush telemetry or dispose expensive resources. + +**Characteristics:** + +- 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`. + +```csharp title="Program.cs" linenums="1" +lambda.OnShutdown(async ( + ITelemetrySink telemetry, + ILogger logger, + CancellationToken ct +) => +{ + try + { + await telemetry.FlushAsync(ct); + logger.LogInformation("Telemetry flushed before shutdown"); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Telemetry flush failed; continuing shutdown"); + } +}); +``` + +### Lifecycle Timeline + +```mermaid +graph LR + A[Cold Start] --> B[OnInit] + B --> C{Init success?} + C -->|Yes| D[Ready] + C -->|No| E[Aborted] + D --> F[Invocation 1] + F --> G[Invocation 2] + G --> H[Invocation 3] + H --> I[...] + I --> J[SIGTERM] + J --> K[OnShutdown] + K --> L[Termination] +``` + +## Dependency Injection Fundamentals + +`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 | +|-----------|--------------------------------------|-------------------------------------------|---------------------------------------------| +| 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 | + +**Tips:** + +- 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. + +### Working with `ILambdaHostContext` + +Every middleware component and handler can ask for `ILambdaHostContext`. Think of it as `HttpContext` for Lambda. + +`ILambdaHostContext` exposes: + +- `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 title="Program.cs" linenums="1" +lambda.MapHandler(async ( + [Event] OrderRequest request, + ILambdaHostContext context, + IOrderService service, + CancellationToken ct +) => +{ + context.Items["RequestId"] = context.AwsRequestId; + + var response = await service.ProcessAsync(request, ct); + context.Features.Get>()!.Response = response; + + return response; +}); +``` + +### Parameter Sources + +Handlers and lifecycle hooks can request multiple parameter types simultaneously: + +- `[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 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) => +{ + var stopwatch = Stopwatch.StartNew(); + await next(context); + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogInformation("Handler finished in {Elapsed}ms", stopwatch.ElapsedMilliseconds); +}); + +lambda.UseMiddleware(async (context, next) => +{ + var eventFeature = context.Features.Get>(); + context.Items["OrderId"] = eventFeature?.Event?.Id; + await next(context); +}); + +lambda.MapHandler(([Event] OrderRequest order) => new OrderResponse(order.Id, true)); +``` + +**Ordering guidance:** + +- 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. + +## Feature System + +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. + +Built-in feature types include: + +- `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. + +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. + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host.Abstractions.Features; + +lambda.UseMiddleware(async (context, next) => +{ + var logger = context.ServiceProvider.GetRequiredService>(); + + if (context.Features.Get>() is { Event: { } request }) + logger.LogInformation("Processing order {OrderId}", request.Id); + + await next(context); + + var responseFeature = context.Features.Get>(); + if (responseFeature?.Response is { } response) + logger.LogInformation("Order {OrderId} complete", response.OrderId); +}); +``` + +### Creating Custom Features + +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. + +```csharp title="DefaultResponseFeatureProvider.cs" linenums="1" +using Amazon.Lambda.Core; + +namespace AwsLambda.Host.Core; + +/// +/// 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 +{ + // ReSharper disable once StaticMemberInGenericType + private static readonly Type FeatureType = typeof(IResponseFeature); + + /// + public bool TryCreate(Type type, out object? feature) + { + feature = type == FeatureType ? new DefaultResponseFeature(lambdaSerializer) : null; + + return feature is not null; + } +} +``` + +Feature providers are just services. Register them like any other dependency: + +```csharp title="Program.cs" linenums="1" +builder.Services.AddSingleton(); +``` + +## Organizing `Program.cs` + +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. + +## Where to Go Next + +- **[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/docs/getting-started/first-lambda.md b/docs/getting-started/first-lambda.md new file mode 100644 index 00000000..0bccdfdc --- /dev/null +++ b/docs/getting-started/first-lambda.md @@ -0,0 +1,622 @@ +# Your First Lambda Function + +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 + +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 + +## Prerequisites + +Before starting, ensure you've completed the [Installation](installation.md) guide and have: + +- `AwsLambda.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. + +!!! 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; +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. Keep +this with your other supporting types at the bottom of `Program.cs`. + +```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 (also placed after the main pipeline). + +```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; +using Microsoft.Extensions.Logging; + +// 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) => + { + var logger = context.ServiceProvider.GetRequiredService>(); + + 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) => + { + var message = service.GetGreeting(request.Name, request.Language); + return new GreetingResponse(message, DateTime.UtcNow); + } +); +``` + +!!! warning "The [Event] Attribute" + 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 + +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; +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( + [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}!"; + } +} +``` + +## 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": "2016-08-12T07:24:18.456Z" +} +``` + +### 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 + +Because middleware uses `ILogger`, the Lambda Test Tool (or CloudWatch Logs) will include entries similar to: + +``` +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 + +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": "2016-08-12T07:24:18.456Z" +} +``` + +## 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**: 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 + +**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 `AwsLambda.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 + +### 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. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 00000000..dc979945 --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,55 @@ +# Getting Started + +**`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 `AwsLambda.Host`? + +- **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 + +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 + +## Start Here + +- **[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. + +Prefer to explore? Head directly to **[Guides](../guides/index.md)** or **[Examples](../examples/index.md)** for deeper dives. + +## Framework Highlights + +- **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. + +## Explore Features + +- **[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: + +- **[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 + +Continue with **[Installation](installation.md)** to configure your project. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 00000000..ef6fb78f --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,246 @@ +# Installation + +This guide walks you through installing `AwsLambda.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 + ``` + + *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 + 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 + + +``` + +### 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 + true + + + + + + +``` + +!!! 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. + +### 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 `AwsLambda.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.md). + +## Troubleshooting + +### Common Issues + +#### C# Language Version Error + +**Error**: `Feature 'interceptors' is not available in C# 10` + +**Solution**: Set `latest` in your `.csproj` file. + +#### 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: + +- 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 `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. + +### Additional Resources + +- [Core Concepts](core-concepts.md) – Understand the framework architecture +- [Examples](../examples/index.md) – Browse complete working examples diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md new file mode 100644 index 00000000..b7975ce8 --- /dev/null +++ b/docs/guides/configuration.md @@ -0,0 +1,370 @@ +# Configuration + +`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. + +## How Configuration Is Built + +`LambdaApplication.CreateBuilder()` wires up the standard .NET defaults: + +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). + +The builder also binds the `AwsLambdaHost` section (from JSON or environment variables) into +`LambdaHostOptions` so framework settings can live next to your app configuration. + +```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: + +```bash +AwsLambdaHost__InvocationCancellationBuffer=00:00:05 +AwsLambdaHost__ClearLambdaOutputFormatting=true +``` + +## LambdaHostOptions Reference + +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 => +{ + options.InitTimeout = TimeSpan.FromSeconds(10); + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(100); + options.ClearLambdaOutputFormatting = true; +}); +``` + +| 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` + +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" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.InitTimeout = TimeSpan.FromSeconds(10); +}); + +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + await cache.WarmUpAsync(ct); // ct fires after 10 seconds + return true; +}); +``` + +### `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. + +```csharp title="Program.cs" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + // Fire cancellation 5 seconds before the Lambda timeout expires + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); +}); + +lambda.MapHandler(async ([Event] Order order, IOrderService service, CancellationToken ct) => + await service.ProcessAsync(order, ct)); +``` + +### `ShutdownDuration` and `ShutdownDurationBuffer` + +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. + +```csharp title="Program.cs" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.ShutdownDuration = ShutdownDuration.ExternalExtensions; // 500 ms + options.ShutdownDurationBuffer = TimeSpan.FromMilliseconds(75); // cancel after 425 ms +}); + +lambda.OnShutdown(async (ITelemetry telemetry, CancellationToken ct) => + await telemetry.FlushAsync(ct)); +``` + +Choose from the provided constants when possible: + +- `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. + +### `ClearLambdaOutputFormatting` + +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" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.ClearLambdaOutputFormatting = true; +}); +``` + +Prefer configuration files? Set `"AwsLambdaHost": { "ClearLambdaOutputFormatting": true }` or the +`AwsLambdaHost__ClearLambdaOutputFormatting` environment variable. + +### `BootstrapHttpClient` + +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" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.BootstrapHttpClient = new HttpClient(new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(5), + Proxy = WebRequest.DefaultWebProxy, + }); +}); +``` + +### `BootstrapOptions` + +`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. + +```csharp title="Program.cs" linenums="1" +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.BootstrapOptions = new LambdaBootstrapOptions + { + Client = LambdaBootstrapOptions.Client.HttpClient, + TelemetryFlushLatency = TimeSpan.FromMilliseconds(25) + }; +}); +``` + +## Application Configuration + +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" +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning" + } + }, + "OrderProcessing": { + "MaxRetries": 3, + "TimeoutSeconds": 30, + "EnableCaching": true + }, + "Database": { + "ConnectionString": "Server=localhost;Database=orders", + "CommandTimeout": 30 + } +} +``` + +Include configuration files in your deployment output so Lambda can read them at runtime: + +```xml title="MyLambda.csproj" + + + PreserveNewest + + + PreserveNewest + + +``` + +### Options Pattern + +Create strongly typed classes and bind them inside `Program.cs`: + +```csharp title="Program.cs" linenums="1" +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing")); + +builder.Services.AddScoped(); +``` + +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; + + public OrderService(IOptions options) => + _options = options.Value; + + public Task ProcessAsync(Order order, CancellationToken ct) => + _options.EnableCaching + ? ProcessWithCacheAsync(order, ct) + : ProcessWithoutCacheAsync(order, ct); +} +``` + +| 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. | + +### Environment-Specific Files + +Ship overrides per environment by naming convention: + +``` +MyLambda/ +├── appsettings.json +├── appsettings.Development.json +└── appsettings.Production.json +``` + +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()`. + +### Environment Variables + +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: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Environment: + Variables: + DATABASE__CONNECTIONSTRING: !Ref DatabaseConnectionString + API_KEY: !Ref ApiKey + AwsLambdaHost__ClearLambdaOutputFormatting: true +``` + +Prefer the double underscore `__` separator when targeting hierarchical keys. + +Access the values directly or through bound options: + +```csharp title="Program.cs" linenums="1" +var apiKey = builder.Configuration["API_KEY"]; + +builder.Services.Configure(options => +{ + options.BaseUrl = builder.Configuration["ExternalApi:BaseUrl"] ?? string.Empty; + options.ApiKey = builder.Configuration["API_KEY"] ?? string.Empty; +}); +``` + +## JSON Serialization Configuration + +`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). + +### Native AOT / Source-Generated 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))] +public partial class MySerializerContext : JsonSerializerContext; + +builder.Services.AddLambdaSerializerWithContext(); +``` + +### Custom Serializer Options + +```csharp title="Program.cs" linenums="1" +builder.Services.AddSingleton(_ => + new DefaultLambdaJsonSerializer(options => + { + options.PropertyNamingPolicy = null; // keep property names as-is + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + }) +); +``` + +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 bound options at startup so bad configuration never reaches production. + +```csharp title="Program.cs" linenums="1" +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("OrderProcessing")) + .ValidateDataAnnotations() + .ValidateOnStart(); +``` + +For more complex rules implement `IValidatableObject` or add a custom validator. + +## Best Practices + +- **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** – Call `Exists()` on the section and ensure the file is copied to +`bin/Release/net8.0/publish`. + +**`appsettings.json` missing in production** – Confirm `PreserveNewest` is set for each config file. + +**Environment variable not loaded** – Remember to add `builder.Configuration.AddEnvironmentVariables()` if you call `CreateBuilder(new LambdaApplicationOptions { DisableDefaults = true })`. + +**LambdaHostOptions ignored** – Verify the JSON is under `"AwsLambdaHost"` or that the environment +variable uses `AwsLambdaHost__OptionName`. + +## Next Steps + +- **[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. diff --git a/docs/guides/dependency-injection.md b/docs/guides/dependency-injection.md new file mode 100644 index 00000000..952d9144 --- /dev/null +++ b/docs/guides/dependency-injection.md @@ -0,0 +1,150 @@ +# Dependency Injection + +`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. + +## How the Container Is Created + +```csharp title="Program.cs" +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddTransient, OrderValidator>(); + +var lambda = builder.Build(); +``` + +- `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. + +## Service Lifetimes in Lambda + +Lambda containers live across multiple invocations. Map the standard lifetimes to Lambda's lifecycle: + +| 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 | + +**Tips:** + +- 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. + +## Invocation Scope and `ILambdaHostContext` + +Every invocation gets its own scope. You can access it via the `ILambdaHostContext` and it is shared +across middleware and handlers: + +```csharp title="Handlers" +lambda.MapHandler(async ( + [Event] OrderRequest request, + IOrderService orders, // scoped service + ILambdaHostContext context, // framework context + CancellationToken cancellation // host-managed token +) => +{ + context.Items["RequestId"] = request.Id; + return await orders.ProcessAsync(request, cancellation); +}); +``` + +`ILambdaHostContext` exposes: + +- `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) + +If your handler doesn't need the Lambda payload, omit the `[Event]` parameter entirely and inject only services. + +!!! tip "Cancellation buffers" + The cancellation token fires slightly **before** AWS kills the process: + + - 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. + +## Middleware and Lifecycle Hooks + +- 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. + +```csharp title="OnInit" +lambda.OnInit(async (services, ct) => +{ + var cache = services.GetRequiredService(); + await cache.WarmUpAsync(ct); + return true; +}); +``` + +## Configuration and Options + +Use the standard options pattern for configuration: + +```csharp title="Program.cs" +builder.Services.Configure( + builder.Configuration.GetSection("OrderProcessing")); +``` + +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. + +## Patterns That Work Well + +- **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]`. + +### Keyed Services in Practice + +```csharp title="Program.cs" +builder.Services.AddKeyedSingleton("email"); +builder.Services.AddKeyedSingleton("sms"); + +lambda.MapHandler(( + [Event] Order order, + [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). + +## Host-Specific Pitfalls + +| 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 + +- 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). + +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 new file mode 100644 index 00000000..00ef16dc --- /dev/null +++ b/docs/guides/error-handling.md @@ -0,0 +1,128 @@ +# Error Handling + +`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 + +- 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(([Event] OrderRequest request, IOrderService service) => + service.ProcessAsync(request) // unhandled exception flows to Lambda runtime +); +``` + +## Prefer Middleware for Error Handling + +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) => +{ + try + { + await next(context); + } + catch (ValidationException 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; + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + // Lambda is about to time out; log and rethrow so the runtime records the failure. + throw; + } +}); +``` + +**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. + +## Handler-Level Try/Catch + +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.MapHandler(async ([Event] CheckoutRequest request, ICheckoutService service) => +{ + try + { + return await service.RunAsync(request); + } + catch (PaymentDeclinedException ex) + { + return new CheckoutResponse("Declined", ex.Message); + } +}); +``` + +## Lifecycle Hooks (OnInit / OnShutdown) + +`LambdaApplication` runs OnInit and OnShutdown handlers outside the invocation pipeline, but each +handler executes in its own DI scope and errors are aggregated: + +- **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.OnInit(async (ICache cache, CancellationToken ct) => +{ + await cache.WarmAsync(ct); + return true; // omit or return false to abort startup +}); + +lambda.OnShutdown(async (ITelemetry telemetry, CancellationToken ct) => +{ + await telemetry.FlushAsync(ct); // exceptions are aggregated and rethrown +}); +``` + +Because the host rethrows aggregate exceptions, CloudWatch logs clearly show every failing handler. + +## Timeouts and Cancellation + +`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" +lambda.MapHandler(async (CancellationToken ct) => +{ + await DoWorkAsync(ct); + // do not swallow OperationCanceledException unless you intentionally handled the timeout +}); +``` + +## Checklist + +- 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 new file mode 100644 index 00000000..1fe15230 --- /dev/null +++ b/docs/guides/handler-registration.md @@ -0,0 +1,204 @@ +# Handler Registration + +`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. + +## Registering a Handler + +```csharp title="Program.cs" linenums="1" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddScoped(); + +var lambda = builder.Build(); + +lambda.MapHandler(([Event] string name, IGreetingService greetings) => + greetings.Greet(name) +); + +await lambda.RunAsync(); + +sealed record GreetingResponse(string Message); + +sealed class GreetingService : IGreetingService +{ + public GreetingResponse Greet(string name) => new($"Hello, {name}!"); +} + +interface IGreetingService +{ + GreetingResponse Greet(string name); +} +``` + +- 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`. + +## 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 `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. +- 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. + +```csharp title="Program.cs" linenums="1" +// No incoming event required +lambda.MapHandler((ILogger logger) => +{ + logger.LogInformation("Heartbeat fired at {Timestamp}", DateTimeOffset.UtcNow); +}); +``` + +### Parameter Sources + +Handlers can mix lambda events with services, context objects, and cancellation tokens. This table shows what the generator knows how to supply: + +| 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" linenums="1" +lambda.MapHandler(async ( + [Event] OrderRequest request, + [FromKeyedServices("primary")] IOrderProcessor orderProcessor, + ILambdaHostContext context, + CancellationToken ct +) => +{ + context.Items["RequestId"] = context.AwsRequestId; + + var response = await orderProcessor.ProcessAsync(request, ct); + context.Properties["OrdersProcessed"] = (int)(context.Properties["OrdersProcessed"] ?? 0) + 1; + + return response; +}); +``` + +`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. + +## Return Values and Serialization + +The generator also emits serialization code for the delegate's return value. Supported shapes include: + +- 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`). + +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. + +## Invocation Scope and Context + +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" linenums="1" +lambda.MapHandler(async ( + [Event] ApiGatewayRequestEnvelope request, + ILambdaHostContext context, + ILogger logger, + CancellationToken ct +) => +{ + var order = request.Body ?? throw new InvalidOperationException("Missing body."); + + if (context.TryGetEvent>(out var originalEnvelope)) + logger.LogDebug("HTTP Method {Method}", originalEnvelope.RequestContext.Http.Method); + + var serviceScope = context.ServiceProvider; + var metrics = serviceScope.GetRequiredService(); + await metrics.AddInvocationAsync(order.Id, ct); + + return new ApiGatewayResponseEnvelope + { + StatusCode = 200, + Body = new(order.Id, approved: true) + }; +}); +``` + +- `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. + +## Source Generation and Diagnostics + +`MapHandler` is decorated as a C# 12 interceptor target. During compilation the generator: + +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. + +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. + +## Patterns and Best Practices + +- 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. + +=== "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`** + +Make sure you call `MapHandler` only once. If you need to branch by trigger type, create separate Lambda projects or use envelope dispatching. + +**`LH0002: No parameter marked with [Event]`** + +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. + +**`InvalidOperationException: No service for type ... has been registered`** + +Register dependencies before building the application: + +```csharp linenums="1" +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddScoped(); +var lambda = builder.Build(); +``` + +**`InvalidOperationException: Unable to resolve service referenced by FromKeyedServicesAttribute`** + +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 + +- [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/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. diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 00000000..b0899c23 --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,134 @@ +# Guides + +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 + +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 + +### [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. + +**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 + +## Learning Path + +### New to `AwsLambda.Host`? + +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? + +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/index.md) for AOT compilation, source generators, and performance optimization. + +## Additional Resources + +- **[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 + +If you encounter issues not covered in these guides: + +- 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..88946cd8 --- /dev/null +++ b/docs/guides/lifecycle-management.md @@ -0,0 +1,223 @@ +# Lifecycle Management + +`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. + +## Execution Flow at a Glance + +``` +Cold Start → OnInit → Invocation 1..N → OnShutdown → Termination +``` + +- **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. + +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 + +```csharp title="Program.cs" +using AwsLambda.Host; +using Microsoft.Extensions.DependencyInjection; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.ClearLambdaOutputFormatting = true; // Stop Lambda from wrapping console output + options.InitTimeout = TimeSpan.FromSeconds(10); // Optional override +}); + +var lambda = builder.Build(); + +lambda.OnInit(async (IDistributedCache cache, ILogger logger, CancellationToken ct) => +{ + logger.LogInformation("Priming cache..."); + await cache.GetStringAsync("preload-key", ct); + return true; // Keep hosting if every handler returns true +}); + +lambda.MapHandler(([Event] Request request) => new Response("OK")); + +await lambda.RunAsync(); +``` + +**OnInit characteristics** + +- 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; `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 + +```csharp title="Program.cs" +lambda.OnInit(async (IConfigLoader config, ILogger logger, CancellationToken ct) => +{ + try + { + await config.LoadAsync(ct); + return true; + } + catch (OperationCanceledException) + { + logger.LogError("Config warmup timed out before InitTimeout."); + return false; + } + catch (Exception ex) + { + logger.LogCritical(ex, "Failed to prepare configuration."); + return false; + } +}); +``` + +If any handler returns `false`, AWS receives an initialization failure and never routes traffic to that execution environment. + +### Clearing Lambda Output Formatting + +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.ClearLambdaOutputFormatting = true; +}); +``` + +You can toggle the same setting from configuration (e.g., `appsettings.json` or environment variables) via the `LambdaHostOptions` binding. + +## OnShutdown: Container Teardown Hooks + +```csharp title="Program.cs" +lambda.OnShutdown(async (ITelemetrySink telemetry, ILogger logger, CancellationToken ct) => +{ + try + { + logger.LogInformation("Flushing telemetry before shutdown."); + await telemetry.FlushAsync(ct); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Telemetry flush failed; continuing shutdown."); + } +}); +``` + +**OnShutdown characteristics** + +- 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. + +### Multiple Handlers + +```csharp title="Program.cs" +lambda.OnShutdown(async (IMetrics metrics, CancellationToken ct) => +{ + await metrics.FlushAsync(ct); +}); + +lambda.OnShutdown(async (IDbConnectionPool pool, CancellationToken ct) => +{ + await pool.DisposeAsync(ct); +}); +``` + +Both handlers are awaited simultaneously. Keep shutdown work small—only the remaining `ShutdownDuration - ShutdownDurationBuffer` window is available. + +## Dependency Scopes and 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.). +- `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" +lambda.OnInit(async ( + IServiceProvider scope, + KeyedService("primary"), + CancellationToken ct +) => +{ + await MyWarmupAsync(scope, MyClient, ct); + return true; +}); +``` + +## Configuring Lifecycle Behavior + +Use `ConfigureLambdaHostOptions` to shape lifecycle behavior centrally or bind the same settings from configuration: + +```csharp title="Program.cs" +builder.Services.ConfigureLambdaHostOptions(options => +{ + 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 +}); +``` + +**Options summary** + +- `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 + +### Warm Databases and Caches + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +lambda.OnInit(async (IDatabaseSessionFactory factory, CancellationToken ct) => +{ + await factory.CreateWarmSessionAsync(ct); + return true; +}); +``` + +### Preload Configuration or Secrets + +```csharp title="Program.cs" +lambda.OnInit(async (ISecretProvider secrets, ILogger logger, CancellationToken ct) => +{ + await secrets.PreloadAsync(ct); + logger.LogInformation("Secrets cached for fast access."); + return true; +}); +``` + +### Flush Telemetry on Shutdown + +```csharp title="Program.cs" +builder.Services.AddSingleton(); + +lambda.OnShutdown(async (ITelemetry telemetry, CancellationToken ct) => +{ + await telemetry.FlushAsync(ct); +}); +``` + +## Best Practices + +- 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 + +- [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. diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md new file mode 100644 index 00000000..19708807 --- /dev/null +++ b/docs/guides/middleware.md @@ -0,0 +1,241 @@ +# Middleware + +`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 that keep middleware and handlers decoupled without extra DI plumbing. + +## Pipeline Basics + +Register middleware before calling `MapHandler`. Components execute in registration order and unwind in +reverse order: + +```csharp title="Program.cs" +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine("[Logging] Before handler"); + await next(context); + Console.WriteLine("[Logging] After handler"); +}); + +lambda.UseMiddleware(async (context, next) => +{ + Console.WriteLine("[Metrics] Before handler"); + await next(context); + Console.WriteLine("[Metrics] After handler"); +}); + +lambda.MapHandler(([Event] Request request) => new Response("ok")); +await lambda.RunAsync(); +``` + +Output: + +``` +[Logging] Before handler +[Metrics] Before handler +[Metrics] After handler +[Logging] After handler +``` + +## `ILambdaHostContext` + +Every middleware receives the same `ILambdaHostContext`, which is scoped to the invocation. + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + var logger = context.ServiceProvider.GetRequiredService>(); + + if (context.CancellationToken.IsCancellationRequested) + { + logger.LogWarning("Invocation cancelled before handler"); + return; + } + + context.Items["RequestId"] = Guid.NewGuid().ToString(); + context.Properties["Version"] ??= "1.0.0"; // safe cross-invocation value + + await next(context); + + var started = (DateTimeOffset)context.Items["Start"]; + logger.LogInformation("Completed in {Duration}ms", (DateTimeOffset.UtcNow - started).TotalMilliseconds); +}); +``` + +Key members: + +- `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` – ASP.NET-style typed capabilities such as `IEventFeature` and `IResponseFeature` that let middleware collaborate without injecting each other. + +## 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 (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 + +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. 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; + +lambda.UseMiddleware(async (context, next) => +{ + var eventFeature = context.Features.Get>(); + if (eventFeature is { Event: { } request }) + Console.WriteLine($"Processing {request.OrderId}"); + + await next(context); + + var responseFeature = context.Features.Get>(); + if (responseFeature?.Response is { } response) + Console.WriteLine($"Result: {response.Status}"); +}); +``` + +Common features: + +| 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 | + +**Why features matter:** + +- 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—register an implementation of `IFeatureProvider` and it becomes available to all middleware. + +### Feature Providers in Practice + +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. + +```csharp title="DefaultResponseFeatureProvider.cs" linenums="1" +using Amazon.Lambda.Core; + +namespace AwsLambda.Host.Core; + +/// +/// 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 +{ + // ReSharper disable once StaticMemberInGenericType + private static readonly Type FeatureType = typeof(IResponseFeature); + + /// + public bool TryCreate(Type type, out object? feature) + { + feature = type == FeatureType ? new DefaultResponseFeature(lambdaSerializer) : null; + + return feature is not null; + } +} +``` + +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: + +```csharp title="Caching" +lambda.UseMiddleware(async (context, next) => +{ + var cache = context.ServiceProvider.GetRequiredService(); + var request = context.Features.Get>()?.Event; + + if (request is not null && cache.TryGet(request.OrderId, out OrderResponse cached)) + { + context.Features.Get>()!.Response = cached; + return; // skip handler + } + + await next(context); +}); +``` + +Wrap the pipeline to catch and translate exceptions: + +```csharp title="Error Handling" +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch (ValidationException ex) + { + var response = context.Features.Get>(); + if (response is not null) + response.Response = new("invalid", ex.Message); + return; // handled + } +}); +``` + +## Ordering Strategy + +Register middleware from outermost to innermost: + +```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 */); +``` + +Guidelines: + +- 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. + +## Configuration and Options + +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. + +## Best Practices + +- **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. +- **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. diff --git a/docs/guides/testing.md b/docs/guides/testing.md new file mode 100644 index 00000000..6d157220 --- /dev/null +++ b/docs/guides/testing.md @@ -0,0 +1,13 @@ +# 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 + diff --git a/docs/index.md b/docs/index.md index 5d43c24e..1b1a0a15 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,16 +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, lifecycle hooks, and async-first design. +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 } +[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 } --- ## 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" @@ -64,7 +70,7 @@ Stop writing boilerplate Lambda code. Start building features with patterns you ``` -=== "aws-lambda-host" +=== "`AwsLambda.Host`" ```csharp // ✅ Familiar .NET Core builder pattern @@ -105,45 +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 scoped lifetime management per invocation. +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-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. +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 } +[OpenTelemetry setup](features/open_telemetry.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 } --- @@ -159,38 +167,18 @@ Create your first Lambda handler: ```csharp using AwsLambda.Host.Builder; -using Microsoft.Extensions.DependencyInjection; -// 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)); +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/index.md) for a complete + tutorial, or explore the [Examples](examples/index.md) to see real-world applications. --- @@ -198,49 +186,48 @@ 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**](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 -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 } + [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/) | +| Package | Description | NuGet | Downloads | +|----------------------------------------------------------------------------------------|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **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 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 -- **[AOT Compilation](examples/aot-example.md)** - Native AOT for optimal cold start performance +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. -[View all examples](examples/){ .md-button } +[Examples (Coming Soon)](examples/index.md){ .md-button } --- @@ -248,26 +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](resources/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/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 -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/index.md){ .md-button .md-button--primary } 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/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}"; -} diff --git a/examples/AwsLambda.Host.Example.OpenTelemetry/README.md b/examples/AwsLambda.Host.Example.OpenTelemetry/README.md index f08f1674..4f4fd119 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 +> 📚 **[View 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/mkdocs.yml b/mkdocs.yml index d8e11a48..42f49ef3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,19 +18,38 @@ theme: - content.tabs.link - navigation.footer - navigation.top - + - navigation.tabs + - navigation.tabs.sticky + - navigation.expand + - navigation.path + - navigation.indexes + - navigation.tracking + - toc.follow + - navigation.instant + - search.highlight + - search.share + - search.suggest + - content.code.annotate + 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: black + accent: orange toggle: icon: material/brightness-4 name: Switch to light mode + font: + code: JetBrains Mono markdown_extensions: # Python Markdown extensions @@ -48,17 +67,54 @@ 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 - admonition + - def_list + - pymdownx.tasklist: + custom_checkbox: true watch: - ./docs/ nav: - - Homepage : index.md + - Home: + - index.md + - Changelog: changelog.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 + - 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 + - Hosting & Builder: guides/hosting.md + - Configuration: guides/configuration.md + - Error Handling: guides/error-handling.md + - Testing: guides/testing.md + - Examples (Coming Soon): + - examples/index.md + - Features: + - features/index.md + - Envelopes: features/envelopes.md + - OpenTelemetry: features/open_telemetry.md + - Advanced (Coming Soon): + - advanced/index.md plugins: - - search \ No newline at end of file + - search + - optimize: + print_gain_summary: false + +extra_css: + - stylesheets/extra.css diff --git a/src/AwsLambda.Host.Abstractions/README.md b/src/AwsLambda.Host.Abstractions/README.md index 18865410..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 diff --git a/src/AwsLambda.Host.OpenTelemetry/README.md b/src/AwsLambda.Host.OpenTelemetry/README.md index dbc4e233..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 diff --git a/src/AwsLambda.Host/README.md b/src/AwsLambda.Host/README.md index fce1849b..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 diff --git a/src/Envelopes/README.md b/src/Envelopes/README.md index 874a786e..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`) 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!"