diff --git a/.agents/skills/expose-a-use-case/SKILL.md b/.agents/skills/expose-a-use-case/SKILL.md index 77d7527..193379e 100644 --- a/.agents/skills/expose-a-use-case/SKILL.md +++ b/.agents/skills/expose-a-use-case/SKILL.md @@ -11,7 +11,7 @@ metadata: Every function is a public method on `Handler` (`src/Presentation/LambdaFunction/LambdaFunction.cs`) with a `[LambdaFunction]` attribute and the signature `Task Name(TRequest request, ILambdaContext context)`. [Lambda Annotations](https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.Annotations) generates a class for each one, such as `Handler_GetMovies_Generated`, and that class is what Lambda calls. It builds the services from `Startup` once per execution environment, creates the `Handler` as a singleton, and writes the function into `src/Presentation/serverless.template` when the project builds. -The `Handler` constructor creates or upgrades the database once. Each method maps its request to the Application request, sends it through the private `Send` helper and maps the result to a presentation model. `Send` opens a DI scope per invocation and cancels the work when the invocation's remaining time runs out. +The `Handler` constructor creates or upgrades the database once. Each method wraps its body in the private `Invoke` helper, which opens the function's trace span and flushes the metrics when the invocation ends, and inside it maps its request to the Application request, sends it through the private `Send` helper and maps the result to a presentation model. `Send` opens a DI scope per invocation and cancels the work when the invocation's remaining time runs out. A list that callers also want over HTTP gets a second method in the `#region HTTP API` block, named with an `Http` prefix (`HttpGetMovies`). It has `[HttpApi(LambdaHttpMethod.Get, "/movies")]` beside `[LambdaFunction]`, takes its inputs as `[FromQuery]` parameters and returns `IHttpResult`. It builds the same request record and calls the direct method through the `Http` helper, which answers `200` with the result and `400` problem details for a `ValidationException`. The build adds the route to the stack's API Gateway HTTP API in `serverless.template`. @@ -22,7 +22,7 @@ Functions contain no logic. Validation, not-found and everything else happen in 1. Add the use case first (`add-a-use-case`). 2. Add the request as a `record` in `src/Presentation/LambdaFunction/Models/` with `init` properties, defaults where the Application request has them, and XML docs on the type and each property. 3. Add or reuse a response `record` in the same folder, with a `` tag for each value. Map Application entities to it with a `partial` method on `LambdaFunctionMapper` (`mapping-with-mapperly`). Lists return `Page` through `ToPage`. -4. Add the method to `Handler` with `[LambdaFunction(ResourceName = "", PackageType = LambdaPackageType.Image, MemorySize = 256, Timeout = 30)]`, and send through `this.Send(..., context)`. Never resolve services or create tokens yourself. +4. Add the method to `Handler` with `[LambdaFunction(ResourceName = "", PackageType = LambdaPackageType.Image, MemorySize = 256, Timeout = 30)]`. Wrap the body in `this.Invoke(request, context, ...)`, so the invocation is traced and its metrics are flushed, and send through `this.Send(..., context)`. Never resolve services or create tokens yourself. 5. Add every new request and response type to `LambdaFunctionJsonSerializerContext` with `[JsonSerializable(typeof(...))]` (`json-serialization`). 6. Document the method: `` with what it returns and in what order, `` for the request and the context, and ``. 7. Test it through the handler in `tests/Presentation.Tests.Integration/HandlerTests.cs` (`presentation-integration-tests`). Cover each success and each validation or not-found failure (`test-design`). @@ -36,14 +36,17 @@ Functions contain no logic. Validation, not-found and everything else happen in [LambdaFunction(ResourceName = "GetAuthors", PackageType = LambdaPackageType.Image, MemorySize = 256, Timeout = 30)] public async Task> GetAuthors(ListRequest request, ILambdaContext context) { - var page = await this.Send(new GetAuthorsQuery + return await this.Invoke(request, context, async () => { - Query = request.Query, - Offset = request.Offset, - Limit = request.Limit - }, context); + var page = await this.Send(new GetAuthorsQuery + { + Query = request.Query, + Offset = request.Offset, + Limit = request.Limit + }, context); - return ToPage(page, this.mapper.ToPresentation); + return ToPage(page, this.mapper.ToPresentation); + }); } ``` @@ -51,6 +54,7 @@ public async Task> GetAuthors(ListRequest request, ILambdaContext c - Return Application entities. Map them to the records in `Models/`, so the function's contract doesn't move when Application does. - Catch exceptions or check `null` in a function. +- Skip `Invoke`. A function that calls `Send` on its own still works, but it leaves no trace and its metrics wait for the next invocation to be sent. - Build a new `ServiceProvider` per invocation, or register services anywhere but `Startup`. The generated class builds them once, and `Send` scopes each call. - Take services as `[FromServices]` parameters. Lambda Annotations 2.4 writes a documentation link for them that doesn't resolve, and the build fails on the warning. - Edit a function's entry in `serverless.template` by hand. The build rewrites it from the attribute. Add other resources and settings beside it.