diff --git a/.agents/skills/git-workflow/shared/conventional-types.md b/.agents/skills/git-workflow/shared/conventional-types.md index 76a0bd25..e1d40033 100644 --- a/.agents/skills/git-workflow/shared/conventional-types.md +++ b/.agents/skills/git-workflow/shared/conventional-types.md @@ -3,7 +3,7 @@ Valid `` values: | Type | Description | SemVer impact | -|------------|-------------------------------------------------------------------|---------------| +| ---------- | ----------------------------------------------------------------- | ------------- | | `feat` | Introduces a new feature | MINOR | | `fix` | Patches a bug | PATCH | | `build` | Changes to the build system or external dependencies | — | diff --git a/.agents/skills/git-workflow/shared/scope-detection.md b/.agents/skills/git-workflow/shared/scope-detection.md index 6bb4c29c..95df6d0a 100644 --- a/.agents/skills/git-workflow/shared/scope-detection.md +++ b/.agents/skills/git-workflow/shared/scope-detection.md @@ -3,7 +3,7 @@ Infer scope from the folder containing the majority of the changes. | Folder | Scope | -|-------------------------------|---------------------| +| ----------------------------- | ------------------- | | `.github/workflows` | `github` | | `src/Core` | `core` | | `src/Abstractions` | `abstractions` | diff --git a/README.md b/README.md index 88a3462a..b84a6c44 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,30 @@ See the [examples directory](./examples/) for more complete examples, including OpenTelemetry integration. For in-memory integration tests, use [MinimalLambda.Testing](./docs/guides/testing.md) (a `WebApplicationFactory`-style runtime shim). +## AI Agent Skill + +This repository includes a `minimal-lambda` agent skill with focused guidance for building, +debugging, testing, and reviewing MinimalLambda code without loading the full repository into agent +context. + +Install it with [skills.sh](https://skills.sh): + +```bash +npx skills add j-d-ha/minimal-lambda --skill minimal-lambda +``` + +Install globally for Claude Code: + +```bash +npx skills add j-d-ha/minimal-lambda --skill minimal-lambda --global --agent claude-code +``` + +Update later with: + +```bash +npx skills update minimal-lambda +``` + ## Documentation - [MinimalLambda](./src/MinimalLambda/README.md) – Core framework documentation diff --git a/docs/index.md b/docs/index.md index 376dbbc6..cbb95250 100644 --- a/docs/index.md +++ b/docs/index.md @@ -183,6 +183,23 @@ await lambda.RunAsync(); 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. +## AI Agent Skill + +Use the bundled `minimal-lambda` agent skill to give coding agents focused MinimalLambda guidance +for handlers, envelopes, middleware, AOT, testing, and repo workflow. + +Install it with [skills.sh](https://skills.sh): + +```bash +npx skills add j-d-ha/minimal-lambda --skill minimal-lambda +``` + +For a global Claude Code install: + +```bash +npx skills add j-d-ha/minimal-lambda --skill minimal-lambda --global --agent claude-code +``` + ______________________________________________________________________ ## Packages diff --git a/skills-lock.json b/skills-lock.json index f2894084..f76e4538 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -4,11 +4,13 @@ "git-workflow": { "source": "LayeredCraft/skills", "sourceType": "github", + "skillPath": "skills/git-workflow/SKILL.md", "computedHash": "fbd78cda46da7d8753beca0d68ce6532589daa4da0a457aa709fe1e11da468fe" }, "zensical-site": { "source": "LayeredCraft/skills", "sourceType": "github", + "skillPath": "skills/zensical-site/SKILL.md", "computedHash": "f66b47704234c8242d45a52e3d92d5307f49254e189819165ee37845da64e9c8" } } diff --git a/skills/minimal-lambda/SKILL.md b/skills/minimal-lambda/SKILL.md new file mode 100644 index 00000000..c70361f0 --- /dev/null +++ b/skills/minimal-lambda/SKILL.md @@ -0,0 +1,93 @@ +--- +name: minimal-lambda +description: Work effectively with MinimalLambda, the Lambda-first .NET hosting framework in this repo and in client projects. Use this skill whenever the user asks to build, debug, migrate, test, document, or review code using MinimalLambda APIs, envelopes, middleware, lifecycle hooks, source-generated handlers, AOT/trimming, OpenTelemetry, or MinimalLambda.Testing. Trigger even when the user only mentions AWS Lambda with Minimal API-style .NET patterns, MapHandler, FromEvent, LambdaApplication, or MinimalLambda package names. +--- + +# MinimalLambda skill + +Use this skill to give agents enough MinimalLambda project context without loading entire repo/docs. + +## First move + +1. Identify task area: + - client project setup/package/config template → read `references/client-project-setup.md` + - app setup/handler/DI/lifecycle → read `references/core-hosting.md` and `references/best-practices.md` + - handler shape/unit-testable handlers → read `references/patterns/handler-patterns.md` + - middleware/features/context → read `references/core-hosting.md` and `references/patterns/middleware-patterns.md` + - lifecycle hooks (`OnInit`/`OnShutdown`) → read `references/core-hosting.md` and + `references/patterns/lifecycle-hook-patterns.md` + - SQS/SNS/API Gateway/Kinesis/Firehose/Kafka/CloudWatch/ALB envelopes → read `references/envelopes.md` and `references/patterns/envelope-patterns.md` + - Native AOT/trimming/serializer context → read `references/patterns/aot-and-envelopes.md` + - integration tests/client project tests → read `references/testing.md` and `references/patterns/testing-patterns.md` + - tracing/metrics/shutdown flush → read `references/opentelemetry.md` + - compile/runtime/test failure → read `references/troubleshooting.md` + - repo contribution/source generator/AOT work → read `references/repo-workflow.md` +2. Use bundled references as the primary source. They are included so the skill works in client projects and global installs without assuming the MinimalLambda repository is present. +3. Only inspect local MinimalLambda source paths after confirming the current workspace is this repository; for repo contributions, read `references/repo-workflow.md` first. +4. Keep Lambda-first constraints in mind: source generation, AOT friendliness, scoped per-invocation services, one handler per runtime execution. + +## Fast mental model + +MinimalLambda = ASP.NET Core Minimal API ergonomics adapted to AWS Lambda: + +```csharp +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddScoped(); + +await using var lambda = builder.Build(); +lambda.MapHandler(([FromEvent] MyEvent evt, IMyService service, CancellationToken ct) => + service.HandleAsync(evt, ct)); + +await lambda.RunAsync(); +``` + +Core pieces: + +- `LambdaApplication.CreateBuilder()` creates standard .NET host/config/DI defaults. +- `MapHandler(...)` registers one Lambda handler. Source generator intercepts it at compile time. +- `[FromEvent]` marks deserialized event payload. At most one payload parameter. +- Other handler parameters resolve from DI/context/keyed services/cancellation token. +- Middleware wraps invocation pipeline via inline `UseMiddleware(...)` or class `UseMiddleware()`. +- `OnInit(...)` runs once during cold start; `OnShutdown(...)` runs during teardown. +- `MinimalLambda.Testing` runs real pipeline in memory for client project tests. +- Envelope packages provide trigger-specific typed event/body access; use matching package rather than hand-parsing AWS records. + +## Portability rule + +Assume this skill may run in a client project, not the MinimalLambda repository. Do not try to read MinimalLambda repo-local source, docs, test, or example paths unless the task is explicitly about changing MinimalLambda itself or the workspace clearly contains this repository. For client-project work, answer from bundled references and the user's project files. + +## Common advice patterns + +Read `references/best-practices.md` before giving architectural advice. + +- Prefer inline `MapHandler` arrow functions, inline middleware, and inline lifecycle hooks in + `Program.cs` when they are Lambda adapter/glue code. +- Keep complex business logic out of handlers, middleware, and hooks; put it in injected services or + small domain helpers. +- Treat `ILambdaInvocationContext`, raw AWS `ILambdaContext`, features, lifecycle context, and other + Lambda context objects as edge concerns. Almost never pass them into services; extract needed + primitive/domain values at the edge. Passing Lambda context into services is usually a + layer-boundary smell. +- Allow simple inline logic in `Program.cs` when logic is tiny and Lambda remains easy to read. +- Extract middleware classes only when middleware is complex, reusable, stateful, or worth testing + separately. +- Prefer `CancellationToken` in async handlers and downstream calls. +- Prefer scoped services for per-invocation state; singleton for reusable clients/caches. +- Avoid storing scoped services in singletons. +- Prefer typed records/responses/envelopes over anonymous response contracts. +- Keep AOT/trimming safe: avoid reflection-heavy dynamic paths unless guarded and tested. +- For direct unit tests, test services/helpers; only extract a named handler when handler adapter + logic itself needs focused tests. +- For end-to-end behavior, use `LambdaApplicationFactory`. + +## Validation checklist + +Before final answer or patch: + +- Does code compile with source generation? `MapHandler` signature has 0 or 1 `[FromEvent]`. +- Does runtime call only one handler mapping path? +- Are packages matched (`MinimalLambda.Testing` same version as `MinimalLambda`)? +- Are envelope package/type and AWS trigger type aligned? +- Are middleware registered before `MapHandler`? +- Are cancellation tokens propagated? +- For repo changes: run format/tests per `AGENTS.md` when practical. diff --git a/skills/minimal-lambda/evals/evals.json b/skills/minimal-lambda/evals/evals.json new file mode 100644 index 00000000..8fd02eed --- /dev/null +++ b/skills/minimal-lambda/evals/evals.json @@ -0,0 +1,72 @@ +{ + "skill_name": "minimal-lambda", + "evals": [ + { + "id": 1, + "prompt": "I have a .NET Lambda using MinimalLambda. Create a clean Program.cs for an order processor with DI, an inline MapHandler arrow function, cancellation token propagation, and a service boundary for non-Lambda business logic. Also mention what package/usings I need.", + "expected_output": "Uses LambdaApplication.CreateBuilder, registers services before Build, maps exactly one inline handler with one [FromEvent] OrderRequest parameter, injects IOrderService and CancellationToken, delegates business logic to the service, calls RunAsync, and explains MinimalLambda package/usings.", + "files": [], + "assertions": [ + { + "text": "Output uses inline MapHandler arrow function as the Lambda adapter and delegates non-trivial business logic to a service." + }, + { + "text": "Handler signature has exactly one [FromEvent] payload parameter and propagates CancellationToken." + }, + { "text": "DI registration happens before builder.Build()." }, + { "text": "Output does not recommend manual JSON parsing or reflection-based dispatch." } + ] + }, + { + "id": 2, + "prompt": "We're building an API Gateway HTTP API v2 Lambda with MinimalLambda and Native AOT. Show the request/response envelope pattern and serializer setup for CreateOrderRequest/CreateOrderResponse.", + "expected_output": "Selects MinimalLambda.Envelopes.ApiGateway, uses ApiGatewayV2RequestEnvelope and ApiGatewayV2ResponseEnvelope or ApiGatewayV2Result, includes JsonSerializerContext entries for envelope and payload/response, registers AddLambdaSerializerWithContext and ConfigureEnvelopeOptions.", + "files": [], + "assertions": [ + { "text": "Output chooses API Gateway v2 envelope/result types, not v1-only types." }, + { "text": "Output registers AddLambdaSerializerWithContext()." }, + { "text": "Output configures envelope options TypeInfoResolver for nested body content." }, + { + "text": "Output includes both envelope and payload/response types in JsonSerializerContext." + } + ] + }, + { + "id": 3, + "prompt": "My MinimalLambda middleware isn't seeing the order request and sometimes scoped data leaks between invocations. Review likely causes and show a better inline middleware pattern, noting when to extract a class or delegate to a service.", + "expected_output": "Explains middleware order before MapHandler, uses ILambdaInvocationContext feature helpers like TryGetEvent/GetResponse, distinguishes Items vs Properties, warns against singleton/scoped leaks, shows thin inline middleware, says to extract class middleware or services only when logic is complex/reusable/stateful/test-worthy, and keeps Lambda context objects out of services.", + "files": [], + "assertions": [ + { "text": "Output says middleware should be registered before MapHandler." }, + { "text": "Output uses context.TryGetEvent() or features for typed event access." }, + { + "text": "Output distinguishes per-invocation Items from cross-invocation Properties/singletons." + }, + { + "text": "Output warns against capturing scoped services/state in singletons." + }, + { + "text": "Output prefers inline middleware for simple Lambda glue and extracts class middleware/services only for complex, reusable, stateful, or separately tested logic." + }, + { + "text": "Output says inline middleware does not support direct service injection and uses context.ServiceProvider, class middleware, or factory middleware for dependencies." + }, + { + "text": "Output does not pass ILambdaInvocationContext, ILambdaContext, or feature collections into application services; it extracts needed domain values at the edge." + } + ] + }, + { + "id": 4, + "prompt": "Add integration tests for a MinimalLambda function. I need to override IOrderService in tests, invoke a typed event, assert success, and know when not to use a shared fixture.", + "expected_output": "Uses MinimalLambda.Testing LambdaApplicationFactory, WithHostBuilder ConfigureServices override, InvokeAsync, WasSuccess assertion, and warns shared factory reuses OnInit/singletons.", + "files": [], + "assertions": [ + { "text": "Output uses LambdaApplicationFactory from MinimalLambda.Testing." }, + { "text": "Output overrides services with WithHostBuilder/ConfigureServices." }, + { "text": "Output invokes typed event using InvokeAsync()." }, + { "text": "Output warns shared fixtures reuse OnInit and singleton state." } + ] + } + ] +} diff --git a/skills/minimal-lambda/references/best-practices.md b/skills/minimal-lambda/references/best-practices.md new file mode 100644 index 00000000..5b194769 --- /dev/null +++ b/skills/minimal-lambda/references/best-practices.md @@ -0,0 +1,161 @@ +# Best practices and decision guide + +Read when designing client-project code, reviewing MinimalLambda usage, or deciding between handler/middleware/lifecycle/testing/envelope patterns. + +## Default architecture + +Use `Program.cs` as Lambda edge: + +1. `Program.cs` wires host, configuration, services, inline middleware, lifecycle hooks, and one + inline `MapHandler` arrow function. +2. Handler/middleware/hooks adapt Lambda concerns: bind payload, access invocation context, set + scopes/features/responses, call application service/helper, return Lambda response. +3. Application services contain complex business logic and can be unit-tested without Lambda host. + +Good shape for real business logic: + +```csharp +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddScoped(); + +await using var lambda = builder.Build(); + +lambda.MapHandler(([FromEvent] OrderRequest request, IOrderService orders, CancellationToken ct) => + orders.ProcessAsync(request, ct)); + +await lambda.RunAsync(); +``` + +Why: handler remains visibly Lambda-shaped and local to startup, while non-Lambda decisions live +behind services/helpers. + +## Handler best practices + +Prefer: + +- one clear `[FromEvent]` payload parameter when event exists +- explicit typed request/response records +- inline `MapHandler` arrow functions for Lambda adapter code +- `CancellationToken` in async handlers +- injected services instead of resolving from `IServiceProvider` +- extracting needed values from `ILambdaInvocationContext`/`ILambdaContext` at the edge before + calling services +- throwing meaningful exceptions for unrecoverable invalid state + +Avoid: + +- complex business rules, orchestration, validation workflows, or persistence logic inside the + handler +- extracting named handler classes just to hold a one-line adapter +- anonymous response contracts in public APIs +- multiple runtime `MapHandler` calls +- manually parsing event JSON when envelope package exists +- reflection-heavy dispatch/routing inside one Lambda unless absolutely needed +- passing `ILambdaInvocationContext`, raw AWS `ILambdaContext`, lifecycle context, feature + collections, or Lambda context wrappers into application services +- injecting Lambda context into services; this should be almost never and treated as a + layer-boundary smell unless explicitly isolated and justified +- storing `ILambdaInvocationContext` or scoped services beyond invocation + +## DI lifetime choices + +| Need | Lifetime | +| ------------------------------------------------ | --------- | +| AWS SDK client, `HttpClient`, config cache | singleton | +| per-invocation repository/unit of work/DbContext | scoped | +| stateless lightweight helper | transient | + +Never capture scoped services in singleton state. Lambda warm reuse makes leaks harder to notice. + +## Middleware best practices + +Use middleware for cross-cutting invocation concerns. Prefer inline middleware in `Program.cs` when +it is app-local Lambda glue. Inline `UseMiddleware(async (context, next) => ...)` receives only +`ILambdaInvocationContext` and `next`; it does not support handler-style direct parameter injection. +Resolve simple dependencies from `context.ServiceProvider`, use `UseMiddleware()` class +middleware for constructor DI, or use `UseMiddleware()` when construction must be +custom/deferred per invocation. Services called from middleware should receive normal domain values, +not Lambda context objects: + +- logging scopes/correlation +- auth/authz +- validation +- metrics/tracing +- idempotency/cache short-circuiting +- error mapping + +Ordering: + +1. diagnostics/tracing/logging +2. auth/authz +3. validation +4. idempotency/caching +5. handler + +Keep inline middleware thin. If logic becomes complex, reusable, stateful, or needs direct unit +tests, extract an `ILambdaMiddleware` class or delegate business work to an injected service. + +## Lifecycle best practices + +Use `OnInit` for cold-start work: + +- warm caches +- validate required configuration/secrets +- pre-create expensive singleton clients only when needed + +Use `OnShutdown` for bounded cleanup: + +- flush telemetry +- drain buffers +- release external leases + +Keep both cancellation-aware. Inline Lambda-specific or tiny hook logic in `Program.cs`; delegate +complex warmup/flush/validation to DI services. Hook delegate overloads support direct DI +parameters, but pass services only the data they need, not lifecycle/context objects. `OnInit` +failures should be intentional because failed init prevents serving invocations. + +## Event source decision guide + +- Plain JSON event: `MinimalLambda` only, `[FromEvent] MyEvent`. +- HTTP API/API Gateway/ALB with JSON body: use matching envelope package and response/result type. +- SQS/SNS/Kinesis/Kafka/Firehose/CloudWatch Logs: use matching envelope package to avoid hand-parsing records. +- Native AOT + envelopes: add `JsonSerializerContext`, `AddLambdaSerializerWithContext()`, and `ConfigureEnvelopeOptions`. + +## Testing strategy + +- Unit-test services/helpers directly; avoid unit-testing generated binding through handler + extraction. +- Use `MinimalLambda.Testing` for pipeline behavior: source-generated binding, middleware, DI scopes, lifecycle, envelopes, serialization, error payloads. +- Share `LambdaApplicationFactory` only when singleton/lifecycle sharing is acceptable. + +## AOT and trimming + +Prefer: + +- source-generated JSON contexts +- inline, analyzable handler delegates +- explicit contracts +- package APIs built for source generation + +Avoid: + +- runtime reflection over handler signatures +- dynamic serialization polymorphism without source-gen metadata +- broad service locator patterns that hide dependencies from code review + +## Code review checklist + +- [ ] One runtime handler mapping. +- [ ] Payload parameter has exactly one `[FromEvent]` or no payload at all. +- [ ] Middleware registered before handler mapping. +- [ ] Handler, middleware, and hooks contain Lambda adapter/glue work only, unless logic is tiny + enough to stay readable inline. +- [ ] Complex business logic lives in injected services/helpers, not handler/middleware/hook bodies. +- [ ] Lambda context objects stay at the edge; services receive domain values/options/cancellation + tokens instead. +- [ ] Async work accepts and propagates cancellation token. +- [ ] DI lifetimes match Lambda warm-container reuse. +- [ ] Envelope package matches AWS trigger. +- [ ] Native AOT path has serializer context and envelope options. +- [ ] Integration tests cover real pipeline when framework behavior matters. diff --git a/skills/minimal-lambda/references/client-project-setup.md b/skills/minimal-lambda/references/client-project-setup.md new file mode 100644 index 00000000..182100b8 --- /dev/null +++ b/skills/minimal-lambda/references/client-project-setup.md @@ -0,0 +1,149 @@ +# Client project setup + +Read when creating or modifying a consumer project that uses MinimalLambda packages. + +## Minimal packages + +Plain Lambda handler: + +```bash +dotnet add package MinimalLambda +``` + +Testing: + +```bash +dotnet add package MinimalLambda.Testing +``` + +OpenTelemetry: + +```bash +dotnet add package MinimalLambda.OpenTelemetry +dotnet add package OpenTelemetry.Extensions.Hosting +``` + +Trigger envelopes: add exactly the matching package, e.g. + +```bash +dotnet add package MinimalLambda.Envelopes.ApiGateway +dotnet add package MinimalLambda.Envelopes.Sqs +``` + +Keep `MinimalLambda.Testing` version aligned with `MinimalLambda`. + +## Project file basics + +Use a current .NET SDK and modern C# language version. Interceptors/source-generation paths require +compiler support; prefer `LangVersion` `latest`/`preview` when package docs or build diagnostics +require it. Repo uses newer versions; client project can use current SDK/LangVersion. + +```xml + + net8.0 + enable + enable + latest + +``` + +For Native AOT, client project also needs normal AWS Lambda AOT settings. Validate with publish, not only build. + +## `Program.cs` template + +```csharp +using MinimalLambda.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddScoped(); + +await using var lambda = builder.Build(); + +lambda.MapHandler(([FromEvent] OrderRequest request, IOrderService orders, CancellationToken cancellationToken) => + orders.ProcessAsync(request, cancellationToken)); + +await lambda.RunAsync(); + +public sealed record OrderRequest(string OrderId); +public sealed record OrderResponse(string OrderId, bool Accepted); + +internal interface IOrderService +{ + Task ProcessAsync(OrderRequest request, CancellationToken cancellationToken); +} + +internal sealed class OrderService : IOrderService +{ + public Task ProcessAsync(OrderRequest request, CancellationToken cancellationToken) => + Task.FromResult(new OrderResponse(request.OrderId, Accepted: true)); +} +``` + +## Configuration + +`CreateBuilder()` loads defaults in documented order and binds MinimalLambda settings from `LambdaHost`. + +`appsettings.json`: + +```json +{ + "LambdaHost": { + "InvocationCancellationBuffer": "00:00:05", + "ClearLambdaOutputFormatting": true + } +} +``` + +Environment variable form: + +```bash +LambdaHost__InvocationCancellationBuffer=00:00:05 +``` + +Code override: + +```csharp +builder.Services.ConfigureLambdaHostOptions(options => +{ + options.InvocationCancellationBuffer = TimeSpan.FromSeconds(5); +}); +``` + +## Top-level statements and tests + +For integration tests that use `LambdaApplicationFactory`, make `Program` visible if needed: + +```csharp +public partial class Program; +``` + +Add it at bottom of `Program.cs` in client app if test project cannot access generated top-level `Program` type. + +## AOT serializer context + +For AOT-friendly JSON serialization: + +```csharp +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(OrderRequest))] +[JsonSerializable(typeof(OrderResponse))] +internal partial class SerializerContext : JsonSerializerContext; + +builder.Services.AddLambdaSerializerWithContext(); +``` + +For envelope payloads, also configure envelope options. See `patterns/aot-and-envelopes.md`. + +## Agent checklist for client setup + +1. Identify trigger and packages. +2. Add `MinimalLambda.Builder` using for builder + `[FromEvent]`. +3. Add service registrations before `Build()`. +4. Add middleware before `MapHandler`. +5. Ensure exactly one `MapHandler` path executes at runtime. +6. Add serializer context for AOT or explicit serialization requirements. +7. Add integration test using `MinimalLambda.Testing` when pipeline behavior matters. diff --git a/skills/minimal-lambda/references/core-hosting.md b/skills/minimal-lambda/references/core-hosting.md new file mode 100644 index 00000000..840620ae --- /dev/null +++ b/skills/minimal-lambda/references/core-hosting.md @@ -0,0 +1,202 @@ +# Core hosting, handlers, DI, lifecycle, middleware + +Read when task touches `LambdaApplication`, `MapHandler`, `[FromEvent]`, DI, lifecycle hooks, middleware, features, configuration, or source-generated handler behavior. + +## Portability note + +This reference is self-contained for client-project use. Do not assume the MinimalLambda source tree exists in the current workspace. If the task is a repo contribution, switch to `repo-workflow.md` for local source landmarks. + +## Builder shape + +Typical app: + +```csharp +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddScoped(); + +await using var lambda = builder.Build(); + +lambda.UseMiddleware(async (context, next) => +{ + await next(context); +}); + +lambda.MapHandler(async ([FromEvent] OrderRequest request, IOrderService service, CancellationToken ct) => + await service.ProcessAsync(request, ct)); + +await lambda.RunAsync(); +``` + +`CreateBuilder()` wires standard .NET configuration/logging/DI defaults unless `LambdaApplicationOptions.DisableDefaults = true`. + +Configuration provider order from docs; later providers override earlier values: + +1. `AWS_` env vars +2. `DOTNET_` env vars +3. `appsettings.json` +4. `appsettings.{Environment}.json` +5. user secrets in Development +6. all env vars + +Framework options bind from `LambdaHost` section, not old `AwsLambdaHost`. + +## Handler registration rules + +- `MapHandler` is source-generated/intercepted. Avoid dynamic delegates/reflection workarounds. +- Multiple `MapHandler` calls may exist in code, but only one can execute at runtime. +- Handler with payload: exactly one `[FromEvent]` parameter. +- Handler with no payload: omit event parameter and omit `[FromEvent]`. +- Other parameters can be services, `[FromKeyedServices(...)]`, `ILambdaInvocationContext`, raw AWS `ILambdaContext`, or `CancellationToken`. +- Return values can be `T`, `Task`, `ValueTask`, `Task`, `ValueTask`; serializer/envelope handles response. + +Good handler style: + +```csharp +lambda.MapHandler(([FromEvent] OrderRequest request, IOrderService service, CancellationToken ct) => + service.ProcessAsync(request, ct)); +``` + +Keep handler inline in `Program.cs` as Lambda adapter code. Put complex business logic in +services/helpers; tiny obvious logic may stay inline. + +## DI and lifetimes + +- Singleton: reused across warm invocations. Good for `HttpClient`, AWS SDK clients, caches, config. +- Scoped: new per invocation. Good default for repositories, DbContexts, per-request state. +- Transient: new per resolve. Good for lightweight helpers. +- Never store scoped service on singleton. +- Prefer constructor/parameter injection over manual `IServiceProvider` resolution. + +## Context and features + +`ILambdaInvocationContext` resembles `HttpContext` for Lambda: + +- `ServiceProvider` scoped to invocation +- `CancellationToken` cancels before hard timeout using configured buffer +- `Items` per-invocation bag +- `Properties` shared cross-invocation dictionary; use thread-safe values +- `Features` typed feature collection +- also exposes AWS Lambda context members + +Keep `ILambdaInvocationContext`, raw AWS `ILambdaContext`, lifecycle context, and feature +collections at the Lambda edge. Handlers/middleware/hooks may read them, then pass primitive/domain +values to services (for example `awsRequestId`, tenant id, headers, payload fields). Almost never +make application services depend on Lambda context types; that usually means Lambda boundary +concerns leaked into application layers. + +Useful feature helpers from docs: + +```csharp +if (context.TryGetEvent(out var request)) { } +if (context.TryGetResponse(out var response)) + context.Features.Get>()!.SetResponse(response); +``` + +Use features in middleware to avoid coupling middleware directly to handlers. + +## Middleware + +Register before `MapHandler`. Execution order follows registration order and unwinds in reverse. + +Inline middleware: quick app-specific glue. Inline middleware receives +`(ILambdaInvocationContext context, LambdaInvocationDelegate next)` only; it does not support direct +service injection like `MapHandler`/hooks. Use `context.ServiceProvider` for simple dependencies, +`UseMiddleware()` class middleware for constructor DI, or `UseMiddleware()` factory +middleware for custom/deferred per-invocation construction. Class middleware constructor parameters +can be resolved from DI and/or explicit `UseMiddleware(args)` values; use `[FromServices]` or +`[FromArguments]` when resolution must be forced. + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogInformation("Before"); + await next(context); + logger.LogInformation("After"); +}); +``` + +Keep middleware inline when it is app-local Lambda glue. Use class middleware when it becomes +complex, reusable, stateful, or worth direct unit tests. + +Class middleware: reusable/testable. + +```csharp +internal sealed class LoggingMiddleware(ILogger logger) : ILambdaMiddleware +{ + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + logger.LogInformation("Invocation starting"); + await next(context); + } +} + +lambda.UseMiddleware(); +``` + +Order guidance: + +- diagnostics first so they wrap all work +- auth before validation/business logic +- short-circuit/caching near handler when it depends on final event/response type + +## Lifecycle + +`OnInit`: + +- cold start once per execution environment +- each handler gets fresh scope +- handlers run concurrently (`Task.WhenAll` per docs) +- `bool`/`Task` can abort startup on `false`; no return implies success +- exceptions aggregate and bubble so container does not serve traffic + +`OnShutdown`: + +- runs once on teardown/SIGTERM +- fresh scope per handler +- bounded by `ShutdownDuration - ShutdownDurationBuffer` +- use to flush telemetry/dispose external resources + +Example: + +```csharp +lambda.OnInit(async (ICache cache, CancellationToken ct) => +{ + await cache.WarmAsync(ct); + return true; +}); + +lambda.OnShutdown(async (ITelemetrySink sink, CancellationToken ct) => +{ + await sink.FlushAsync(ct); +}); +``` + +Keep hooks inline when they are Lambda lifecycle glue or tiny. Hook delegates support DI parameters, +so prefer `lambda.OnInit((ICache cache, CancellationToken ct) => ...)` over manual service +resolution. Delegate complex warmup, health validation, telemetry flushing, or external cleanup to +DI services. Do not pass lifecycle/context objects to services; pass cancellation tokens and +domain/config values. + +## Options + +Use `builder.Services.ConfigureLambdaHostOptions(options => { ... })`. + +Important options: + +- `InitTimeout` default 5s +- `InvocationCancellationBuffer` default 500ms +- `ShutdownDuration` default external extension window (500ms) +- `ShutdownDurationBuffer` default 50ms +- `ClearLambdaOutputFormatting` +- `BootstrapHttpClient` +- `BootstrapOptions` + +## Common pitfalls + +- Missing `[FromEvent]` for payload handler → generator diagnostic. +- Duplicate `[FromEvent]` → generator diagnostic. +- Registering middleware after `MapHandler` likely means it will not wrap as intended. +- Multiple `MapHandler` runtime calls → `InvalidOperationException`. +- Manual service resolution everywhere → less testable; prefer injected params. +- Ignoring cancellation token → bad Lambda timeout behavior. diff --git a/skills/minimal-lambda/references/envelopes.md b/skills/minimal-lambda/references/envelopes.md new file mode 100644 index 00000000..b7e89a14 --- /dev/null +++ b/skills/minimal-lambda/references/envelopes.md @@ -0,0 +1,90 @@ +# Envelopes + +Read when task touches SQS, SNS, API Gateway, Kinesis, Kinesis Firehose, Kafka/MSK, CloudWatch Logs, ALB, event bodies, typed payloads, or AWS trigger-specific request/response types. + +## Portability note + +This reference is self-contained for client-project use. Do not assume envelope package source or tests exist in the current workspace. If the task is a repo contribution, switch to `repo-workflow.md` before inspecting local source. + +## Mental model + +Envelope packages wrap official AWS Lambda event classes and add type-safe payload access, commonly `BodyContent`, so client code avoids manual JSON parsing of strings. + +Benefits: + +- typed payload contracts +- trigger-specific envelope support +- AOT-friendly serializer context paths +- reuse AWS event shape while adding generic body content + +## Package selection + +Use only package(s) matching trigger: + +| Trigger | Package | +| ------------------------------- | ------------------------------------------------- | +| SQS | `MinimalLambda.Envelopes.Sqs` | +| SNS | `MinimalLambda.Envelopes.Sns` | +| SNS-to-SQS | `MinimalLambda.Envelopes.Sqs` SNS-to-SQS envelope | +| API Gateway REST/HTTP/WebSocket | `MinimalLambda.Envelopes.ApiGateway` | +| Kinesis Data Streams | `MinimalLambda.Envelopes.Kinesis` | +| Kinesis Firehose transform | `MinimalLambda.Envelopes.KinesisFirehose` | +| Kafka/MSK/self-managed | `MinimalLambda.Envelopes.Kafka` | +| CloudWatch Logs | `MinimalLambda.Envelopes.CloudWatchLogs` | +| ALB | `MinimalLambda.Envelopes.Alb` | + +## Exact common types + +| Trigger | Request/event type | Response type | +| -------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- | +| API Gateway REST/HTTP v1/WebSocket | `ApiGatewayRequestEnvelope` | `ApiGatewayResponseEnvelope` or `ApiGatewayResult` | +| API Gateway HTTP API v2 / Function URL | `ApiGatewayV2RequestEnvelope` | `ApiGatewayV2ResponseEnvelope` or `ApiGatewayV2Result` | +| ALB | `AlbRequestEnvelope` | `AlbResponseEnvelope` or `AlbResult` | +| SQS | `SqsEnvelope` | usually none | +| SNS | `SnsEnvelope` | usually none | +| SNS-to-SQS | `SqsSnsEnvelope` | usually none | +| Kinesis Data Streams | `KinesisEnvelope` | usually none | +| Kinesis Firehose transform | `KinesisFirehoseEventEnvelope` | `KinesisFirehoseResponseEnvelope` | +| Kafka/MSK/self-managed | `KafkaEnvelope` | usually none | +| CloudWatch Logs | `CloudWatchLogsEnvelope` or `CloudWatchLogsEnvelope` | usually none | + +## Handler shape + +Envelope types still enter through `[FromEvent]`: + +```csharp +lambda.MapHandler(async ([FromEvent] SqsEnvelope envelope, IOrderService service, CancellationToken ct) => +{ + foreach (var message in envelope.Records) + { + if (message.BodyContent is not null) + await service.ProcessAsync(message.BodyContent, ct); + } +}); +``` + +When exact type/member names matter, inspect target package README/source if available. In client +projects/global installs where source is unavailable, rely on bundled references plus installed +package docs/IntelliSense. Names vary by trigger. + +## AOT / serialization guidance + +- Prefer explicit records/classes with predictable JSON contracts. +- For Native AOT, add envelope and payload/response types to `JsonSerializerContext`. +- Register `builder.Services.AddLambdaSerializerWithContext()`. +- For nested envelope payloads, also call `builder.Services.ConfigureEnvelopeOptions(options => options.JsonOptions.TypeInfoResolver = SerializerContext.Default)`. +- Avoid ad-hoc `JsonSerializer.Deserialize` and reflection-based polymorphism. +- Match request and response envelope types for API Gateway/ALB-style triggers. + +See `patterns/aot-and-envelopes.md` for complete snippets. + +## Agent workflow for envelope questions + +1. Identify AWS event source. +2. Read matching envelope README/package docs when available. +3. Inspect source/tests for exact type/member names only when available; otherwise use package + docs/IntelliSense. +4. Check production batch semantics for queue/stream triggers, especially partial-batch failure + support. +5. Propose minimal package references and handler signature. +6. Add/adjust tests using `MinimalLambda.Testing` or existing envelope unit test style. diff --git a/skills/minimal-lambda/references/opentelemetry.md b/skills/minimal-lambda/references/opentelemetry.md new file mode 100644 index 00000000..6b02737e --- /dev/null +++ b/skills/minimal-lambda/references/opentelemetry.md @@ -0,0 +1,83 @@ +# OpenTelemetry + +Read when task asks for tracing, metrics, X-Ray/OTLP, `UseOpenTelemetryTracing`, AWS Lambda instrumentation, or telemetry flush on shutdown. + +## Portability note + +This reference is self-contained for client-project use. Do not assume MinimalLambda OpenTelemetry source, examples, or tests exist in the current workspace. If the task is a repo contribution, switch to `repo-workflow.md` before inspecting local source. + +## Packages + +Typical packages: + +```bash +dotnet add package MinimalLambda.OpenTelemetry +dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol +dotnet add package OpenTelemetry.Extensions.Hosting +``` + +X-Ray often also needs: + +```bash +dotnet add package OpenTelemetry.Contrib.Extensions.AWSXRay +``` + +## Basic setup + +```csharp +var builder = LambdaApplication.CreateBuilder(); + +builder.Services + .AddOpenTelemetry() + .WithTracing(tracing => + { + tracing.AddAWSLambdaConfigurations(); + tracing.AddSource("MyService"); + tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MyService")); + tracing.AddOtlpExporter(); + }) + .WithMetrics(metrics => + { + metrics.AddMeter("MyService"); + metrics.AddOtlpExporter(); + }); + +await using var lambda = builder.Build(); + +lambda.UseOpenTelemetryTracing(); +lambda.OnShutdownFlushOpenTelemetry(); + +lambda.MapHandler(async ([FromEvent] Request request, ILogger logger, CancellationToken ct) => +{ + logger.LogInformation("Handling {Name}", request.Name); + return new Response($"Hello {request.Name}"); +}); + +await lambda.RunAsync(); +``` + +## How it works + +`UseOpenTelemetryTracing()` adds invocation middleware. It reads event and response through feature collection and delegates root-span creation/context propagation to official `OpenTelemetry.Instrumentation.AWSLambda`. + +`OnShutdownFlushOpenTelemetry()` registers shutdown hook to force-flush tracer and meter providers before Lambda freezes/terminates environment. + +## Rules and pitfalls + +- Configure OpenTelemetry services before `builder.Build()`. +- Call `lambda.UseOpenTelemetryTracing()` before `MapHandler` so tracing wraps handler. +- Register `TracerProvider`; startup fails if required provider missing. +- Add custom `ActivitySource` names via `AddSource` and custom `Meter` names via `AddMeter`. +- Use shutdown flush for buffered exporters. +- Keep exporter config Lambda-safe; avoid long flush windows beyond shutdown budget. + +## Agent workflow + +1. Identify backend: OTLP, X-Ray, console, vendor exporter. +2. Add required NuGet packages. +3. Configure `AddOpenTelemetry()` with tracing/metrics. +4. Add `UseOpenTelemetryTracing()` and shutdown flush. +5. Validate source names/meters match app instrumentation. +6. For tests, inspect OpenTelemetry unit tests for exact assertions/patterns only when working in + this repo or when test sources are available; otherwise use bundled references and public package + docs. diff --git a/skills/minimal-lambda/references/patterns/aot-and-envelopes.md b/skills/minimal-lambda/references/patterns/aot-and-envelopes.md new file mode 100644 index 00000000..c5925529 --- /dev/null +++ b/skills/minimal-lambda/references/patterns/aot-and-envelopes.md @@ -0,0 +1,73 @@ +# AOT and serializer patterns + +Read when user targets Native AOT, trimming, or envelope body deserialization. + +## Plain event/response context + +```csharp +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(OrderRequest))] +[JsonSerializable(typeof(OrderResponse))] +internal partial class SerializerContext : JsonSerializerContext; + +builder.Services.AddLambdaSerializerWithContext(); +``` + +`AddLambdaSerializerWithContext()` registers AWS `ILambdaSerializer` backed by source-generated metadata. + +## Envelope context + +Envelope packages deserialize in two steps: + +1. Lambda serializer deserializes raw AWS event/envelope. +2. Envelope code deserializes nested body/message/record payload. + +So register both Lambda serializer and envelope options. + +```csharp +using System.Text.Json.Serialization; +using MinimalLambda.Envelopes.ApiGateway; + +[JsonSerializable(typeof(ApiGatewayRequestEnvelope))] +[JsonSerializable(typeof(ApiGatewayResponseEnvelope))] +[JsonSerializable(typeof(CreateOrderRequest))] +[JsonSerializable(typeof(CreateOrderResponse))] +internal partial class SerializerContext : JsonSerializerContext; + +builder.Services.AddLambdaSerializerWithContext(); + +builder.Services.ConfigureEnvelopeOptions(options => +{ + options.JsonOptions.TypeInfoResolver = SerializerContext.Default; +}); +``` + +## Kinesis example + +Docs show this pattern: + +```csharp +[JsonSerializable(typeof(KinesisEnvelope))] +[JsonSerializable(typeof(StreamRecord))] +internal partial class SerializerContext : JsonSerializerContext; + +builder.Services.AddLambdaSerializerWithContext(); + +builder.Services.ConfigureEnvelopeOptions(options => +{ + options.JsonOptions.TypeInfoResolver = SerializerContext.Default; +}); +``` + +## AOT review checklist + +- [ ] Every event/envelope/response/payload type appears in `JsonSerializerContext`. +- [ ] `AddLambdaSerializerWithContext()` is registered before `Build()`. +- [ ] Envelope payloads also configure `ConfigureEnvelopeOptions`. +- [ ] No runtime reflection over handlers/contracts. +- [ ] Publish has been tested; build alone is not enough. + +## When not to overdo it + +If client project is not AOT/trimming-sensitive, normal `System.Text.Json` fallback may be acceptable. Still prefer explicit contracts and avoid dynamic serialization for Lambda cold-start performance and reliability. diff --git a/skills/minimal-lambda/references/patterns/envelope-patterns.md b/skills/minimal-lambda/references/patterns/envelope-patterns.md new file mode 100644 index 00000000..e1c86efc --- /dev/null +++ b/skills/minimal-lambda/references/patterns/envelope-patterns.md @@ -0,0 +1,100 @@ +# Envelope patterns + +Read when selecting or implementing strongly typed AWS trigger envelopes. + +## Exact envelope types + +| Trigger | Request/event type | Response type | +| -------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- | +| API Gateway REST/HTTP v1/WebSocket | `ApiGatewayRequestEnvelope` | `ApiGatewayResponseEnvelope` or `ApiGatewayResult` | +| API Gateway HTTP API v2 / Function URL | `ApiGatewayV2RequestEnvelope` | `ApiGatewayV2ResponseEnvelope` or `ApiGatewayV2Result` | +| ALB | `AlbRequestEnvelope` | `AlbResponseEnvelope` or `AlbResult` | +| SQS | `SqsEnvelope` | usually no response | +| SNS | `SnsEnvelope` | usually no response | +| SNS-to-SQS | `SqsSnsEnvelope` | usually no response | +| Kinesis Data Streams | `KinesisEnvelope` | usually no response | +| Kinesis Firehose transform | `KinesisFirehoseEventEnvelope` | `KinesisFirehoseResponseEnvelope` | +| Kafka/MSK | `KafkaEnvelope` | usually no response | +| CloudWatch Logs | `CloudWatchLogsEnvelope` or `CloudWatchLogsEnvelope` | usually no response | + +Always inspect matching README/source for current property names and special cases when available. +In client projects/global installs without source, use bundled references plus package +docs/IntelliSense. + +## API Gateway result pattern + +Use result builders when handler can return different response body types. + +```csharp +lambda.MapHandler(([FromEvent] ApiGatewayRequestEnvelope request) => +{ + if (request.BodyContent is null) + return ApiGatewayResult.BadRequest(new ErrorResponse("Missing body")); + + return ApiGatewayResult.Created(new CreateOrderResponse(request.BodyContent.OrderId)); +}); +``` + +Use envelope response when response type is stable and you need full control. + +```csharp +lambda.MapHandler(([FromEvent] ApiGatewayRequestEnvelope request) => + new ApiGatewayResponseEnvelope + { + StatusCode = 200, + BodyContent = new CreateOrderResponse(request.BodyContent!.OrderId), + Headers = new Dictionary { ["Content-Type"] = "application/json" }, + }); +``` + +## SQS batch pattern + +```csharp +lambda.MapHandler(async ([FromEvent] SqsEnvelope envelope, IOrderService orders, CancellationToken ct) => +{ + foreach (var message in envelope.Records) + { + if (message.BodyContent is null) + continue; + + await orders.ProcessAsync(message.BodyContent, ct); + } +}); +``` + +Production code often needs partial-batch failure support depending on AWS integration. Check package/docs/current support before promising behavior. + +## Kinesis pattern + +```csharp +lambda.MapHandler(async ([FromEvent] KinesisEnvelope envelope, IStreamProcessor processor, CancellationToken ct) => +{ + foreach (var record in envelope.Records) + { + if (record.Kinesis.DataContent is not null) + await processor.ProcessAsync(record.Kinesis.DataContent, ct); + } +}); +``` + +Kinesis payload appears on `record.Kinesis.DataContent`. + +## Firehose transform pattern + +```csharp +lambda.MapHandler(([FromEvent] KinesisFirehoseEventEnvelope envelope) => +{ + var response = new KinesisFirehoseResponseEnvelope(); + + // Inspect package README/source for current record-construction API. + // Preserve record IDs and set transform result per AWS Firehose contract. + + return response; +}); +``` + +Firehose response contracts are easy to get wrong; use the current package documentation or repo workflow before changing framework code. + +## AOT envelope pattern + +See `aot-and-envelopes.md`. Register both Lambda serializer and envelope options because raw event and nested body content deserialize at different layers. diff --git a/skills/minimal-lambda/references/patterns/handler-patterns.md b/skills/minimal-lambda/references/patterns/handler-patterns.md new file mode 100644 index 00000000..33497e50 --- /dev/null +++ b/skills/minimal-lambda/references/patterns/handler-patterns.md @@ -0,0 +1,127 @@ +# Handler patterns + +Read when implementing or reviewing handler shape. + +## Default: inline Lambda adapter in `Program.cs` + +Prefer mapping an inline arrow function in `Program.cs` for normal MinimalLambda apps. + +```csharp +lambda.MapHandler(([FromEvent] OrderRequest request, IOrderService orders, CancellationToken ct) => + orders.ProcessAsync(request, ct)); +``` + +Handler job: Lambda edge only. + +- Bind payload with `[FromEvent]`. +- Accept Lambda/context parameters only when needed. +- Keep Lambda context objects at edge; pass services domain values, not `ILambdaInvocationContext`/ + `ILambdaContext`. +- Accept injected services explicitly. +- Pass `CancellationToken` through. +- Return response shape. + +Business job: service/helper. + +- validation workflows +- authorization/business policy decisions +- persistence +- external API orchestration +- transformations large enough to need names/tests + +Why: readers see whole Lambda entry point in one place, source generator gets analyzable signature, +business code remains independent of Lambda. + +## Complex business logic: delegate immediately + +```csharp +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddScoped(); + +await using var lambda = builder.Build(); + +lambda.MapHandler(([FromEvent] OrderRequest request, IOrderService orders, CancellationToken ct) => + orders.ProcessAsync(request, ct)); + +await lambda.RunAsync(); +``` + +Do not move complex logic into a named handler class just to hide it. Move it into app +services/domain helpers because that code is not Lambda-specific. + +## Tiny logic: keep it inline + +Small, obvious logic is fine directly in `Program.cs`. + +```csharp +lambda.MapHandler(([FromEvent] PingRequest request) => + new PingResponse(request.Message.Trim(), DateTimeOffset.UtcNow)); +``` + +Keep inline when it is easier to read than a service, has no persistence/external orchestration, and +probably does not need isolated unit tests. + +## No-event handler + +Use for scheduled/heartbeat style Lambda where payload not needed. + +```csharp +lambda.MapHandler(async (IJobRunner jobs, CancellationToken ct) => +{ + await jobs.RunAsync(ct); +}); +``` + +No `[FromEvent]`; no fake unused event parameter. + +## Context-aware handler + +Use `ILambdaInvocationContext` when handler needs AWS request metadata, per-invocation bag, or features. + +```csharp +lambda.MapHandler(async ( + [FromEvent] OrderRequest request, + ILambdaInvocationContext context, + IOrderService orders, + CancellationToken ct) => +{ + context.Items["OrderId"] = request.OrderId; + context.Items["AwsRequestId"] = context.AwsRequestId; + return await orders.ProcessAsync(request, ct); +}); +``` + +Use context sparingly. Prefer services for business operations. Do not pass +`ILambdaInvocationContext`, raw AWS `ILambdaContext`, features, or Lambda wrappers into +domain/application services. Extract needed values (`AwsRequestId`, deadline, tenant id, claims, +headers) in handler/middleware and pass those values instead. Only isolate a service behind Lambda +context when boundary cannot be expressed otherwise. + +## Keyed service handler + +Use .NET keyed services for explicit variant selection. + +```csharp +builder.Services.AddKeyedScoped("primary"); + +lambda.MapHandler(( + [FromEvent] OrderRequest request, + [FromKeyedServices("primary")] IOrderProcessor processor, + CancellationToken ct) => + processor.ProcessAsync(request, ct)); +``` + +Keep keys simple constants. + +## Testing guidance + +Unit-test services/helpers for business behavior. Use integration tests for source-generated +binding, DI, middleware, envelopes, and serialization. + +Extract a named static handler only when the adapter itself has enough Lambda-specific branching to +deserve direct unit tests. That should be uncommon. + +## Anti-pattern: routing many event shapes in one handler + +Avoid big `object`/JSON switch dispatch when separate Lambda functions or explicit envelope types fit. It hides contracts from source generation, tests, and AOT serializer metadata. diff --git a/skills/minimal-lambda/references/patterns/lifecycle-hook-patterns.md b/skills/minimal-lambda/references/patterns/lifecycle-hook-patterns.md new file mode 100644 index 00000000..37293a0b --- /dev/null +++ b/skills/minimal-lambda/references/patterns/lifecycle-hook-patterns.md @@ -0,0 +1,95 @@ +# Lifecycle hook patterns + +Read when adding or reviewing `OnInit` and `OnShutdown` hooks. + +## Default: inline lifecycle glue in `Program.cs` + +Use hooks for Lambda lifecycle concerns: + +- cold-start cache warmup +- required configuration/secrets validation +- expensive singleton preflight +- telemetry/buffer flush during shutdown +- bounded external cleanup + +Keep hooks inline when logic is small or Lambda-specific. + +```csharp +lambda.OnInit(async (ICacheWarmer warmer, CancellationToken ct) => +{ + await warmer.WarmAsync(ct); + return true; +}); + +lambda.OnShutdown(async (ITelemetrySink telemetry, CancellationToken ct) => +{ + await telemetry.FlushAsync(ct); +}); +``` + +Unlike inline middleware, lifecycle hook delegate overloads support direct DI parameters. Prefer DI +parameters over manual service resolution. + +## Delegate complex work to services + +Hooks should coordinate lifecycle; services should do real work. + +```csharp +lambda.OnInit(async (IStartupChecks checks, CancellationToken ct) => + await checks.ValidateAsync(ct)); +``` + +Good service inputs: + +- `CancellationToken` +- options/config values +- primitive/domain values +- typed abstractions like `ICacheWarmer`, `IStartupChecks`, `ITelemetrySink` + +Avoid service inputs: + +- `ILambdaLifecycleContext` +- `ILambdaInvocationContext` +- raw AWS `ILambdaContext` +- feature collections or Lambda wrappers + +Passing lifecycle/context objects into services is almost always a layer-boundary smell. Extract +values at the Lambda edge if a service needs them. + +## `OnInit` return values + +Return `bool` only when startup should be able to abort. + +```csharp +lambda.OnInit(async (IStartupChecks checks, CancellationToken ct) => +{ + var ok = await checks.ValidateAsync(ct); + return ok; +}); +``` + +- `true` continues startup. +- `false` aborts startup and prevents serving invocations. +- no return value implies success. + +Use `false` intentionally; failed init means Lambda should not process events. + +## Cancellation and time bounds + +Always accept/pass `CancellationToken` for async hook work. + +- `OnInit` token is bounded by `LambdaHostOptions.InitTimeout`. +- `OnShutdown` token is bounded by shutdown duration/buffer. +- Downstream services should honor cancellation promptly. + +## Shared state + +Lifecycle hooks run outside normal invocation flow. + +- `OnInit` runs once per execution environment. +- `OnShutdown` runs once during teardown. +- Each hook handler gets fresh scope. +- Warm containers reuse singleton state after init. + +Do not use lifecycle hooks for per-invocation state. Use scoped services or middleware for +invocation data. diff --git a/skills/minimal-lambda/references/patterns/middleware-patterns.md b/skills/minimal-lambda/references/patterns/middleware-patterns.md new file mode 100644 index 00000000..a3b1704b --- /dev/null +++ b/skills/minimal-lambda/references/patterns/middleware-patterns.md @@ -0,0 +1,131 @@ +# Middleware patterns + +Read when adding logging, metrics, validation, auth, idempotency, response mapping, or feature access. + +Default: inline app-local middleware in `Program.cs`. Middleware should mostly be Lambda pipeline +glue: read context/features, set scopes/items/responses, call `next`, or delegate to services. +Inline middleware receives only `context` and `next`; it does not support direct service injection. +Resolve simple dependencies from `context.ServiceProvider`, use `UseMiddleware()` class +middleware when constructor DI is cleaner, or use `UseMiddleware()` when construction must +be custom/deferred per invocation. Class middleware constructor parameters can come from DI and/or +explicit `UseMiddleware(args)` values; use `[FromServices]` or `[FromArguments]` to remove +ambiguity. Keep Lambda context objects at this edge; services should receive domain values, options, +and `CancellationToken`, not `ILambdaInvocationContext`/`ILambdaContext`. Extract class middleware +when logic is complex, reusable, stateful, or needs direct unit tests. + +## Inline logging/correlation + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + var logger = context.ServiceProvider.GetRequiredService>(); + var correlationId = context.AwsRequestId; + + using var scope = logger.BeginScope(new Dictionary + { + ["AwsRequestId"] = correlationId, + }); + + context.Items["CorrelationId"] = correlationId; + + await next(context); +}); +``` + +Good for app-local glue. Keep heavy logic in services. + +## Feature-based validation + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + if (!context.TryGetEvent(out var request)) + { + await next(context); + return; + } + + if (string.IsNullOrWhiteSpace(request.OrderId)) + { + context.Features.Get>()! + .SetResponse(new OrderResponse("", Accepted: false)); + return; + } + + await next(context); +}); +``` + +Features let middleware work with typed event/response without coupling to handler implementation. + +## Class-based middleware + +```csharp +internal sealed class TimingMiddleware(ILogger logger) : ILambdaMiddleware +{ + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + var started = Stopwatch.GetTimestamp(); + + try + { + await next(context); + } + finally + { + var elapsed = Stopwatch.GetElapsedTime(started); + logger.LogInformation("Invocation completed in {ElapsedMs} ms", elapsed.TotalMilliseconds); + } + } +} + +lambda.UseMiddleware(); +``` + +Use class middleware for reusable code, complex pipeline behavior, stateful middleware, or easier +unit tests. Do not extract a class just to hide a small inline delegate. + +## Short-circuit cache + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + var cache = context.ServiceProvider.GetRequiredService(); + + if (context.TryGetEvent(out var request) + && await cache.TryGetAsync(request.OrderId, context.CancellationToken) is { } cached) + { + context.Features.Get>()!.SetResponse(cached); + return; + } + + await next(context); + + if (request is not null && context.GetResponse() is { } response) + await cache.SetAsync(request.OrderId, response, context.CancellationToken); +}); +``` + +Place short-circuit middleware after auth/validation and before handler. If cache policy grows +beyond simple Lambda pipeline glue, delegate policy decisions to an injected service and pass +request ids/keys/values, not the whole Lambda context. + +## Error boundary pattern + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + try + { + await next(context); + } + catch (ValidationException ex) + { + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogWarning(ex, "Validation failed"); + context.Features.Get>()!.SetResponse(new ErrorResponse(ex.Message)); + } +}); +``` + +Use trigger-specific HTTP result/envelope for API Gateway/ALB when mapping errors to status codes. diff --git a/skills/minimal-lambda/references/patterns/testing-patterns.md b/skills/minimal-lambda/references/patterns/testing-patterns.md new file mode 100644 index 00000000..08785a28 --- /dev/null +++ b/skills/minimal-lambda/references/patterns/testing-patterns.md @@ -0,0 +1,93 @@ +# Testing patterns + +Read when adding client-project tests with `MinimalLambda.Testing`. + +## End-to-end happy path + +```csharp +public sealed class OrderLambdaTests +{ + [Fact] + public async Task InvokeAsync_ReturnsAcceptedOrder() + { + await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + + var response = await factory.TestServer.InvokeAsync( + new OrderRequest("order-123"), + TestContext.Current.CancellationToken); + + response.WasSuccess.Should().BeTrue(); + response.Response.Should().Be(new OrderResponse("order-123", Accepted: true)); + } +} +``` + +`InvokeAsync` starts host on demand. Call `StartAsync` explicitly when init status matters. + +## Assert startup/init behavior + +```csharp +await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + +var init = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); + +init.InitStatus.Should().Be(InitStatus.InitCompleted); +``` + +Use fresh factory per test when checking lifecycle hooks. + +## Override services + +```csharp +await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + { + builder.ConfigureServices((_, services) => + { + services.RemoveAll(); + services.AddScoped(_ => Substitute.For()); + }); + }); +``` + +Use this for external dependencies. Do not mock MinimalLambda runtime when runtime behavior is under test. + +## No-event handler + +```csharp +var response = await factory.TestServer.InvokeNoEventAsync( + TestContext.Current.CancellationToken); + +response.WasSuccess.Should().BeTrue(); +``` + +## No-response handler + +```csharp +var response = await factory.TestServer.InvokeNoResponseAsync( + new JobRequest("sync"), + TestContext.Current.CancellationToken); + +response.WasSuccess.Should().BeTrue(); +``` + +## Error assertion + +```csharp +var response = await factory.TestServer.InvokeAsync( + new OrderRequest("bad"), + TestContext.Current.CancellationToken); + +response.WasSuccess.Should().BeFalse(); +response.Error.Should().NotBeNull(); +``` + +Assert structured Lambda-style error payload, not local exception type, for invocation failures. + +## Shared factory fixture + +Use `IClassFixture>` for speed only when shared singletons and one-time `OnInit` are acceptable. + +Avoid shared factory when tests mutate configuration, singleton state, or lifecycle assertions. diff --git a/skills/minimal-lambda/references/repo-workflow.md b/skills/minimal-lambda/references/repo-workflow.md new file mode 100644 index 00000000..15a06ccd --- /dev/null +++ b/skills/minimal-lambda/references/repo-workflow.md @@ -0,0 +1,98 @@ +# MinimalLambda repo workflow and implementation notes + +Read when changing MinimalLambda itself, source generators, packages, docs, examples, AOT compatibility, or tests. + +## Repo guardrails + +Follow root `AGENTS.md`: + +- small focused diffs +- match existing patterns +- run formatting + tests before handoff when practical +- avoid reflection-heavy/dynamic code unless required and guarded +- Lambda-first and AOT-friendly + +## Commands + +Restore: + +```bash +DOTNET_NOLOGO=1 dotnet restore +DOTNET_NOLOGO=1 dotnet tool restore +``` + +Build: + +```bash +DOTNET_NOLOGO=1 dotnet build --configuration Release --no-restore /p:TreatWarningsAsErrors=true +``` + +Tests: + +```bash +task test:all +# or pick the target framework relevant to the change +DOTNET_NOLOGO=1 dotnet test --configuration Release -f net10.0 +``` + +Use `task test:all` when practical; focused `dotnet test -f ...` commands are shortcuts, not the +canonical full suite. + +AOT check: + +```bash +DOTNET_NOLOGO=1 dotnet publish src/AotCompatibility.TestApp/AotCompatibility.TestApp.csproj /p:TreatWarningsAsErrors=true +``` + +Format: + +```bash +task format +# or task format:csharpier for C# formatting only +``` + +## Code style + +- nullable enabled; treat nullability warnings as bugs +- C# preview/C# 14 features present +- extension blocks `extension(...) { ... }` are intentional; do not rewrite to old extension syntax +- file-scoped namespaces +- `sealed` for public classes unless inheritance intended +- `internal` for implementation details +- prefer `ArgumentNullException.ThrowIfNull(arg)` +- avoid dynamic/reflection on hot paths and source-generated/AOT paths + +## Source generator landmarks + +- entry: `src/MinimalLambda.SourceGenerators/MinimalLambdaGenerator.cs` +- syntax providers: `SyntaxProviders/` +- models: `Models/Handlers/`, `Models/Middleware/` +- diagnostics: `Diagnostics/` +- emitters: `Emitters/` +- templates: search for `.scriban` +- tests/snapshots: `tests/MinimalLambda.SourceGenerators.UnitTests/` + +Generator responsibilities include: + +- intercepting `MapHandler`, lifecycle hooks, class middleware registration +- validating `[FromEvent]` count and keyed service metadata +- generating reflection-free invocation glue + +## Runtime landmarks + +- `src/MinimalLambda/Builder/` app builder, invocation/lifecycle builders, extension targets +- `src/MinimalLambda/Core/Context/` invocation/lifecycle contexts +- `src/MinimalLambda/Core/Features/` event/response/features +- `src/MinimalLambda/Runtime/` hosted service/bootstrap integration +- `src/MinimalLambda.Abstractions/` public contracts + +## Test strategy + +- Unit tests for isolated runtime/generator behavior. +- Snapshot tests for generated code changes; update snapshots only when intended. +- Integration tests via `MinimalLambda.Testing` for pipeline behavior. +- AOT test app for trimming/native publish compatibility. + +## Before final handoff + +Report commands run and results. If not run, say why. diff --git a/skills/minimal-lambda/references/testing.md b/skills/minimal-lambda/references/testing.md new file mode 100644 index 00000000..addcc5a5 --- /dev/null +++ b/skills/minimal-lambda/references/testing.md @@ -0,0 +1,96 @@ +# Testing client projects with MinimalLambda.Testing + +Read when task asks for integration tests, in-memory Lambda execution, test fixtures, host overrides, lifecycle tests, or client project test setup. + +## Portability note + +This reference is self-contained for client-project use. Do not assume MinimalLambda test-source paths exist in the current workspace. If the task is a repo contribution, switch to `repo-workflow.md` before inspecting local tests. + +## Core idea + +`MinimalLambda.Testing` behaves like ASP.NET Core `WebApplicationFactory`: boot real Lambda entry point in memory and speak same Runtime API contract as AWS. + +Use for: + +- source-generated handler coverage +- middleware/envelope/DI/lifecycle integration +- host customization in tests +- regression tests for error payloads and cold-start behavior + +Prefer plain unit tests for isolated business logic. + +## Package rule + +`MinimalLambda.Testing` version should match `MinimalLambda` version. + +## Basic xUnit shape + +```csharp +await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + +var initResult = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); +initResult.InitStatus.Should().Be(InitStatus.InitCompleted); + +var response = await factory.TestServer.InvokeAsync( + new MyEvent("World"), + TestContext.Current.CancellationToken); + +response.WasSuccess.Should().BeTrue(); +response.Response.Message.Should().Be("Hello World!"); +``` + +## Invocation APIs + +- `InvokeAsync(event, token)` for typed input + typed output. +- `InvokeNoEventAsync(token)` for no event payload. +- `InvokeNoResponseAsync(event, token)` for no response body. + +Responses expose: + +- `WasSuccess` +- `Response` +- `Error` for structured Lambda-style failure payload + +## Host customization + +Use `WithHostBuilder` for test-only config/services. + +```csharp +await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + { + builder.ConfigureServices((_, services) => + { + services.RemoveAll(); + services.AddScoped(); + }); + }); +``` + +Also supports app configuration overrides and custom service provider factories. + +## Shared fixtures caution + +Using one factory across many tests improves speed but shares: + +- `OnInit` once +- `OnShutdown` once at fixture disposal +- singleton services across tests + +Do not share factory when testing init/shutdown behavior or singleton isolation. + +## Cancellation/timeouts + +- Use `WithCancellationToken` to flow test cancellation. +- Per-call tokens bound individual invokes. +- `ServerOptions.FunctionTimeout` defaults to 3 seconds; adjust to test timeout behavior. + +## Agent workflow + +1. Check client target framework/test framework. +2. Add matching `MinimalLambda.Testing` package. +3. Ensure `Program` accessible to test project if needed (`public partial class Program` pattern if client uses top-level statements). +4. Pick right invoke API. +5. Assert `WasSuccess` before accessing response. +6. Test error cases through `Error`, not raw exceptions unless host startup fails. diff --git a/skills/minimal-lambda/references/troubleshooting.md b/skills/minimal-lambda/references/troubleshooting.md new file mode 100644 index 00000000..35ea9553 --- /dev/null +++ b/skills/minimal-lambda/references/troubleshooting.md @@ -0,0 +1,106 @@ +# Troubleshooting MinimalLambda usage + +Read when user reports compile errors, source generator diagnostics, runtime startup failures, handler not running, serialization issues, middleware not firing, or test failures. + +## Source generator / compile-time issues + +### `MapHandler` call not intercepted + +Symptoms: + +- runtime exception: `This method is replaced at compile time.` +- generated handler missing + +Check: + +- project uses supported C# language version +- package references include `MinimalLambda` +- `MapHandler` call shape is a static, analyzable inline delegate or method group +- source generator diagnostics in build output + +### Missing or duplicate `[FromEvent]` + +Payload handler needs exactly one `[FromEvent]` parameter. No-payload handler should have no event parameter. + +Bad: + +```csharp +lambda.MapHandler((OrderRequest request, IOrderService service) => service.Process(request)); +``` + +Good: + +```csharp +lambda.MapHandler(([FromEvent] OrderRequest request, IOrderService service) => service.Process(request)); +``` + +### Keyed service diagnostic + +`[FromKeyedServices(...)]` keys must be supported constants. If generator reports unsupported metadata, replace complex key object with string/int/enum-style supported key. + +## Runtime issues + +### Multiple handlers registered + +Only one handler can be registered per Lambda execution. Conditional mapping is fine; executing multiple mappings is not. + +### DI service missing + +Handler parameters not marked `[FromEvent]` resolve from DI/context. If a custom service parameter fails, register it before `builder.Build()`. + +### Scoped service leak + +Warm Lambda containers reuse singletons. If per-invocation state appears in later invocations, check singleton fields and static state for captured scoped services/data. + +### Middleware not running + +Check registration order. Middleware should be registered before `MapHandler`. + +## Serialization/envelope issues + +### Body content null + +For API Gateway/ALB/SQS/etc. envelope types, inspect raw event shape and matching package README. Common causes: + +- wrong envelope type for trigger version +- request body absent or not JSON +- AOT serializer context missing payload/envelope type +- custom content type unsupported by default JSON envelope + +### Native AOT serialization failure + +Add all event, envelope, payload, and response types to `JsonSerializerContext`. Register both Lambda serializer and envelope options when envelopes deserialize nested payloads. + +```csharp +builder.Services.AddLambdaSerializerWithContext(); +builder.Services.ConfigureEnvelopeOptions(options => +{ + options.JsonOptions.TypeInfoResolver = SerializerContext.Default; +}); +``` + +## Testing issues + +### `LambdaApplicationFactory` cannot access `Program` + +Add public partial class marker at bottom of top-level `Program.cs`: + +```csharp +public partial class Program; +``` + +### Test passes alone, fails in class fixture + +Shared factory reuses host/singletons and runs `OnInit` once. Use fresh factory when test needs isolation. + +### Invocation times out + +Check `factory.ServerOptions.FunctionTimeout`, long-running middleware, and cancellation token propagation. + +## Debug workflow + +1. Reproduce with `dotnet build` first for generator diagnostics. +2. Run a focused integration test with `MinimalLambda.Testing`. +3. Log event/response feature types in middleware if binding unclear. +4. Compare handler/envelope type with matching package README and source. +5. For AOT, publish the app; normal build is insufficient. diff --git a/skills/minimal-lambda/scripts/validate_references.py b/skills/minimal-lambda/scripts/validate_references.py new file mode 100755 index 00000000..350d51f0 --- /dev/null +++ b/skills/minimal-lambda/scripts/validate_references.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Validate MinimalLambda skill references against repo layout. + +Run from repo root: + python skills/minimal-lambda/scripts/validate_references.py +""" + +from __future__ import annotations + +from pathlib import Path +import re +import sys + +ROOT = Path.cwd() +SKILL = ROOT / "skills" / "minimal-lambda" + +REQUIRED_FILES = [ + SKILL / "SKILL.md", + SKILL / "references" / "core-hosting.md", + SKILL / "references" / "best-practices.md", + SKILL / "references" / "client-project-setup.md", + SKILL / "references" / "envelopes.md", + SKILL / "references" / "testing.md", + SKILL / "references" / "opentelemetry.md", + SKILL / "references" / "troubleshooting.md", + SKILL / "references" / "repo-workflow.md", + SKILL / "references" / "patterns" / "handler-patterns.md", + SKILL / "references" / "patterns" / "middleware-patterns.md", + SKILL / "references" / "patterns" / "envelope-patterns.md", + SKILL / "references" / "patterns" / "testing-patterns.md", + SKILL / "references" / "patterns" / "aot-and-envelopes.md", + SKILL / "evals" / "evals.json", +] + +SYMBOL_CHECKS = { + "LambdaApplication": "src/MinimalLambda/Builder/LambdaApplication.cs", + "MapHandler": "src/MinimalLambda/Builder/InterceptionTargets/MapHandlerLambdaApplicationExtensions.cs", + "FromEventAttribute": "src/MinimalLambda.Abstractions/Attributes/FromEventAttribute.cs", + "AddLambdaSerializerWithContext": "src/MinimalLambda/Builder/Extensions/SerializerServiceCollectionExtensions.cs", + "ConfigureEnvelopeOptions": "src/MinimalLambda/Builder/Extensions/ConfigurationServiceCollectionExtensions.cs", + "LambdaApplicationFactory": "src/MinimalLambda.Testing/LambdaApplicationFactory.cs", + "ApiGatewayV2RequestEnvelope": "src/Envelopes/MinimalLambda.Envelopes.ApiGateway/ApiGatewayV2RequestEnvelope.cs", + "SqsEnvelope": "src/Envelopes/MinimalLambda.Envelopes.Sqs/SqsEnvelope.cs", + "KinesisEnvelope": "src/Envelopes/MinimalLambda.Envelopes.Kinesis/KinesisEnvelope.cs", +} + + +def fail(message: str) -> None: + print(f"FAIL: {message}") + sys.exit(1) + + +def main() -> None: + missing = [path for path in REQUIRED_FILES if not path.exists()] + if missing: + fail("missing skill files:\n" + "\n".join(str(p) for p in missing)) + + for symbol, rel_path in SYMBOL_CHECKS.items(): + path = ROOT / rel_path + if not path.exists(): + fail(f"symbol source path missing for {symbol}: {rel_path}") + if symbol not in path.read_text(encoding="utf-8"): + fail(f"symbol {symbol} not found in {rel_path}") + + skill_text = "\n".join(p.read_text(encoding="utf-8") for p in SKILL.rglob("*.md")) + if re.search(r"\.Response\s*=", skill_text): + fail("docs use non-existent IResponseFeature.Response setter; use SetResponse(...)") + + client_reference_text = "\n".join( + p.read_text(encoding="utf-8") + for p in SKILL.rglob("*.md") + if p.name != "repo-workflow.md" + ) + if re.search(r"`(?:src|docs|tests|examples)/", client_reference_text): + fail("client-facing skill references should not point at MinimalLambda repo-local paths") + + print("OK: MinimalLambda skill references validated") + + +if __name__ == "__main__": + main() diff --git a/tasks/LocalDevTasks.yml b/tasks/LocalDevTasks.yml index 50d60db9..fdda65ea 100644 --- a/tasks/LocalDevTasks.yml +++ b/tasks/LocalDevTasks.yml @@ -59,11 +59,11 @@ tasks: - dotnet lambda-test-tool start --lambda-emulator-port 5050 --config-storage-path ./lambda_test_tool docs:serve: - desc: Serve docs locally with Zensical + desc: Serve docs locally with Zensical and open browser silent: true cmds: - echo "📖 Serving Docs" - - uv run zensical serve -f mkdocs.yml + - uv run zensical serve -f mkdocs.yml --open docs:build: desc: Build docs site with Zensical