diff --git a/Directory.Build.props b/Directory.Build.props index bf4cdeab..78b44622 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.0.0-beta.6 + 2.0.0-beta.7 MIT diff --git a/Directory.Packages.props b/Directory.Packages.props index 462cf1fd..aef561bd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,6 +17,7 @@ + diff --git a/MinimalLambda.sln b/MinimalLambda.sln index 8413ae0f..10dd44a9 100644 --- a/MinimalLambda.sln +++ b/MinimalLambda.sln @@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalLambda.Testing.UnitT EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lambdas", "Lambdas", "{D9109C8A-AFA8-49C8-A19C-381500902B4D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalLambda.Envelopes", "src\Envelopes\MinimalLambda.Envelopes\MinimalLambda.Envelopes.csproj", "{190CC9C7-007F-48C5-867D-03B911D53397}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -435,6 +437,18 @@ Global {381F49C0-297F-4B61-8A82-E9E502E523AD}.Release|x64.Build.0 = Release|Any CPU {381F49C0-297F-4B61-8A82-E9E502E523AD}.Release|x86.ActiveCfg = Release|Any CPU {381F49C0-297F-4B61-8A82-E9E502E523AD}.Release|x86.Build.0 = Release|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Debug|Any CPU.Build.0 = Debug|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Debug|x64.ActiveCfg = Debug|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Debug|x64.Build.0 = Debug|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Debug|x86.ActiveCfg = Debug|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Debug|x86.Build.0 = Debug|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Release|Any CPU.ActiveCfg = Release|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Release|Any CPU.Build.0 = Release|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Release|x64.ActiveCfg = Release|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Release|x64.Build.0 = Release|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Release|x86.ActiveCfg = Release|Any CPU + {190CC9C7-007F-48C5-867D-03B911D53397}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -471,5 +485,6 @@ Global {7350482B-DFDE-4DCD-A0C5-899D5EA00F74} = {D9109C8A-AFA8-49C8-A19C-381500902B4D} {381F49C0-297F-4B61-8A82-E9E502E523AD} = {D9109C8A-AFA8-49C8-A19C-381500902B4D} {7E7B3004-C6C4-4A8C-8610-2A1CB61F834A} = {D9109C8A-AFA8-49C8-A19C-381500902B4D} + {190CC9C7-007F-48C5-867D-03B911D53397} = {1C3C52D9-2936-4A0C-A9C8-F330F22B8359} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 666eab07..57801514 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ including SQS, SNS, API Gateway, Kinesis, and more. | Package | NuGet | Downloads | |------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [**MinimalLambda.Envelopes**](./src/Envelopes/MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](./MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](./MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](./MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/docs/features/envelopes.md b/docs/features/envelopes.md index 4ecbed5b..384a01b6 100644 --- a/docs/features/envelopes.md +++ b/docs/features/envelopes.md @@ -26,6 +26,7 @@ Install only the envelopes you need; each one lives in its own NuGet package. | Event Source | Package | NuGet | |---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Infrastructure / Base | [MinimalLambda.Envelopes](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | | SQS | [MinimalLambda.Envelopes.Sqs](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.Sqs) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | | SNS | [MinimalLambda.Envelopes.Sns](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.Sns) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | | API Gateway / HTTP API | [MinimalLambda.Envelopes.ApiGateway](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.ApiGateway) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | @@ -35,6 +36,11 @@ Install only the envelopes you need; each one lives in its own NuGet package. | CloudWatch Logs | [MinimalLambda.Envelopes.CloudWatchLogs](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.CloudWatchLogs) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.CloudWatchLogs) | | Application Load Balancer (ALB) | [MinimalLambda.Envelopes.Alb](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.Alb) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Alb.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Alb) | +!!! note "Infrastructure Package" + `MinimalLambda.Envelopes` is automatically referenced by ALB and API Gateway packages. It provides + `IHttpResult` and extension methods for the response builder API. You don't need to install + it directly. + Each package ships with README examples in the repository if you need event-specific guidance. ## Quick Start @@ -75,6 +81,125 @@ await lambda.RunAsync(); internal sealed record OrderMessage(string OrderId, decimal Amount); ``` +## Response Builder API + +For HTTP-based event sources (API Gateway, ALB), result classes provide a fluent API for building +responses. **Key benefit**: Return multiple strongly typed models from the same handler—for example, +different success and error response types. + +```csharp title="API Gateway Example" linenums="1" +using MinimalLambda.Envelopes.ApiGateway; + +lambda.MapHandler(([Event] ApiGatewayRequestEnvelope request) => +{ + // Each return statement uses a different strongly typed model + if (string.IsNullOrEmpty(request.BodyContent?.Username)) + return ApiGatewayResult.BadRequest(new ValidationError("Username required")); + + if (!authService.Authenticate(request.BodyContent)) + return ApiGatewayResult.Unauthorized(new AuthError("Invalid credentials")); + + return ApiGatewayResult.Ok(new LoginSuccess(token, expiresAt)); +}); + +internal record LoginRequest(string Username, string Password); +internal record ValidationError(string Message); +internal record AuthError(string Message); +internal record LoginSuccess(string Token, DateTime ExpiresAt); +``` + +### Available Result Classes + +| Class | Package | Use Case | +|------------------------|--------------------------------------|---------------------------------------------| +| `AlbResult` | MinimalLambda.Envelopes.Alb | Application Load Balancer responses | +| `ApiGatewayResult` | MinimalLambda.Envelopes.ApiGateway | REST API / HTTP API v1 / WebSocket | +| `ApiGatewayV2Result` | MinimalLambda.Envelopes.ApiGateway | HTTP API v2 responses | + +Common methods: `Ok()`, `Created()`, `NoContent()`, `BadRequest()`, `Unauthorized()`, `NotFound()`, +`Conflict()`, `UnprocessableEntity()`, `InternalServerError()`, `StatusCode(int)`, `Text(int, +string)`, `Json(int, T)`. All methods have overloads with and without body content. + +### When to Use Results vs. Envelopes + +**Use result classes** when you need to return multiple strongly typed models from the same handler. +Provides convenient methods for common HTTP status codes. + +**Use envelope classes directly** when you need custom serialization (e.g., XML) or want to extend +envelope base classes for custom behavior. + +!!! tip "Complete API Reference" + For detailed method documentation, AOT configuration, and advanced usage, see the package README + files: + + - [ALB Package README](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.Alb) + - [API Gateway Package README](https://github.com/j-d-ha/minimal-lambda/tree/main/src/Envelopes/MinimalLambda.Envelopes.ApiGateway) + +!!! note + Result classes use their respective envelope classes internally (`AlbResponseEnvelope`, + `ApiGatewayResponseEnvelope`, etc.). They're a convenience layer over the envelope + infrastructure. + +## AOT Support + +When using .NET Native AOT, register all envelope and payload types in your `JsonSerializerContext`. + +!!! tip "Register Both Envelope and Payload Types" + You must register **both** the envelope type (e.g., `ApiGatewayRequestEnvelope`) + **and** the inner payload type (e.g., `LoginRequest`). The envelope wraps the AWS event + structure, while the payload is your business type inside the envelope. + +### Basic Envelope Setup + +```csharp title="Program.cs" linenums="1" +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(ApiGatewayRequestEnvelope))] // Envelope wrapper +[JsonSerializable(typeof(ApiGatewayResponseEnvelope))] // Envelope wrapper +[JsonSerializable(typeof(LoginRequest))] // Inner payload type +[JsonSerializable(typeof(LoginSuccess))] // Inner payload type +internal partial class SerializerContext : JsonSerializerContext; +``` + +### Result Classes with Multiple Return Types + +When using result classes (`AlbResult`, `ApiGatewayResult`, `ApiGatewayV2Result`), register each +response type separately: + +```csharp title="Program.cs" linenums="1" +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(ApiGatewayRequestEnvelope))] +[JsonSerializable(typeof(ApiGatewayResult))] +[JsonSerializable(typeof(LoginRequest))] +[JsonSerializable(typeof(ValidationError))] +[JsonSerializable(typeof(AuthError))] +[JsonSerializable(typeof(LoginSuccess))] +internal partial class SerializerContext : JsonSerializerContext; +``` + +### Registering the Serializer Context + +Register the serializer and configure envelope options to use the context: + +```csharp title="Program.cs" linenums="1" +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddLambdaSerializerWithContext(); + +builder.Services.ConfigureEnvelopeOptions(options => +{ + options.JsonOptions.TypeInfoResolver = SerializerContext.Default; +}); +``` + +!!! important "Why Register in Two Places?" + The context must be registered as the type resolver for **both** the envelope options and the + Lambda serializer because deserialization happens at different steps: + + 1. **Lambda serializer** deserializes the raw AWS event (e.g., API Gateway event structure) + 2. **Envelope options** deserialize the envelope content into your payload types + ## Custom Serialization & EnvelopeOptions All envelope packages respect the global `EnvelopeOptions` configuration. Call diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 9e5c4465..3cfb7288 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -190,6 +190,7 @@ Envelope packages provide type-safe, strongly-typed event handling for specific | Package | Event Source | When to Use | |----------------------------------------------|---------------------------|-------------------------------| +| **MinimalLambda.Envelopes** | Infrastructure | HTTP response builders (auto-referenced) | | **MinimalLambda.Envelopes.Sqs** | Amazon SQS | Processing SQS queue messages | | **MinimalLambda.Envelopes.Sns** | Amazon SNS | Handling SNS notifications | | **MinimalLambda.Envelopes.ApiGateway** | API Gateway | Building REST/HTTP APIs | diff --git a/docs/index.md b/docs/index.md index 69aedfed..a566c3ef 100644 --- a/docs/index.md +++ b/docs/index.md @@ -95,12 +95,7 @@ already know, while still embracing Lambda’s execution model. var greeting = await service.GreetAsync(request.BodyContent.Name, cancellationToken); // ✅ Type-safe response - automatic JSON serialization - return new ApiGatewayResponseEnvelope - { - BodyContent = new GreetingResponse(greeting, DateTime.UtcNow), - StatusCode = 200, - Headers = new Dictionary { ["Content-Type"] = "application/json" }, - }; + return ApiGatewayResult.Ok(new GreetingResponse(greeting, DateTime.UtcNow)); } ); @@ -211,6 +206,7 @@ deserialization. | Package | Description | NuGet | Downloads | |----------------------------------------------------------------------------------------|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **MinimalLambda.Envelopes** | Infrastructure package for HTTP response builders | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | **MinimalLambda.Envelopes.Sqs** | Simple Queue Service events with typed message bodies | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | **MinimalLambda.Envelopes.Sns** | Simple Notification Service messages | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | | **MinimalLambda.Envelopes.ApiGateway** | REST, HTTP, and WebSocket APIs | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | diff --git a/examples/MinimalLambda.Example.Events/Program.cs b/examples/MinimalLambda.Example.Events/Program.cs index 752238b1..61199221 100644 --- a/examples/MinimalLambda.Example.Events/Program.cs +++ b/examples/MinimalLambda.Example.Events/Program.cs @@ -1,11 +1,11 @@ using System; -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MinimalLambda.Builder; +using MinimalLambda.Envelopes; using MinimalLambda.Envelopes.ApiGateway; var builder = LambdaApplication.CreateBuilder(); @@ -30,12 +30,21 @@ { logger.LogInformation("In Handler. Payload: {Payload}", request.Body); - return new ApiGatewayResponseEnvelope - { - BodyContent = new Response($"Hello {request.BodyContent?.Name}!", DateTime.UtcNow), - StatusCode = 201, - Headers = new Dictionary { ["Content-Type"] = "application/json" }, - }; + if (request.BodyContent == null) + return ApiGatewayResult.InternalServerError(new BadErrorDetails("Bad error")); + + if (request.BodyContent.Name == "error") + return ApiGatewayResult + .BadRequest(new ErrorDetails("bummer")) + .Customize(response => + { + response.Headers.Add("X-Custom-Header", "Custom Value"); + response.MultiValueHeaders.Add("X-Custom-Header", ["Custom Value 2"]); + }); + + return ApiGatewayResult + .Ok(new Response($"Hello {request.BodyContent?.Name}!", DateTime.UtcNow)) + .Customize(result => result.Headers.Add("X-Custom-Header", "Custom Value")); } ); @@ -43,10 +52,15 @@ internal record Response(string Message, DateTime TimestampUtc); +internal record ErrorDetails(string Message); + +internal record BadErrorDetails(string Message); + internal record Request(string Name); [JsonSerializable(typeof(ApiGatewayRequestEnvelope))] -[JsonSerializable(typeof(ApiGatewayResponseEnvelope))] +[JsonSerializable(typeof(ApiGatewayResult))] [JsonSerializable(typeof(Request))] +[JsonSerializable(typeof(ErrorDetails))] [JsonSerializable(typeof(Response))] internal partial class SerializerContext : JsonSerializerContext; diff --git a/lambda_test_tool/SavedRequests/__DefaultFunction__/API Gateway - Basic.json b/lambda_test_tool/SavedRequests/__DefaultFunction__/API Gateway - Basic.json new file mode 100644 index 00000000..8b46b39d --- /dev/null +++ b/lambda_test_tool/SavedRequests/__DefaultFunction__/API Gateway - Basic.json @@ -0,0 +1,123 @@ +{ + "body": "{\"name\":\"jonas\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": true, + "queryStringParameters": { + "foo": "bar" + }, + "multiValueQueryStringParameters": { + "foo": [ + "bar" + ] + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ], + "Accept-Encoding": [ + "gzip, deflate, sdch" + ], + "Accept-Language": [ + "en-US,en;q=0.8" + ], + "Cache-Control": [ + "max-age=0" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Host": [ + "0123456789.execute-api.us-east-1.amazonaws.com" + ], + "Upgrade-Insecure-Requests": [ + "1" + ], + "User-Agent": [ + "Custom User Agent String" + ], + "Via": [ + "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" + ], + "X-Forwarded-For": [ + "127.0.0.1, 127.0.0.2" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/src/Envelopes/MinimalLambda.Envelopes.Alb/AlbResult.cs b/src/Envelopes/MinimalLambda.Envelopes.Alb/AlbResult.cs new file mode 100644 index 00000000..c0196dd8 --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes.Alb/AlbResult.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using MinimalLambda.Options; + +namespace MinimalLambda.Envelopes.Alb; + +/// +/// Represents an HTTP response for AWS Lambda functions invoked by an Application Load +/// Balancer (ALB). +/// +/// +/// This class wraps and provides support for +/// response envelope customization through . Use this type when +/// returning responses from Lambda functions triggered by ALB target groups. +/// +public sealed class AlbResult : ApplicationLoadBalancerResponse, IHttpResult +{ + [JsonIgnore] + private readonly IResponseEnvelope? _inner; + + private AlbResult(ApplicationLoadBalancerResponse response) + { + _inner = response as IResponseEnvelope; + StatusCode = response.StatusCode; + StatusDescription = response.StatusDescription; + Headers = response.Headers; + MultiValueHeaders = response.MultiValueHeaders; + Body = response.Body; + IsBase64Encoded = response.IsBase64Encoded; + } + + /// + public void PackPayload(EnvelopeOptions options) + { + if (_inner is null) + return; + + _inner.PackPayload(options); + Body = ((ApplicationLoadBalancerResponse)_inner).Body; + } + + /// + public static AlbResult Create( + int statusCode, + T? bodyContent, + IDictionary headers, + bool isBase64Encoded + ) => + new( + new AlbResponseEnvelope + { + StatusCode = statusCode, + BodyContent = bodyContent, + Headers = headers, + IsBase64Encoded = isBase64Encoded, + } + ); + + /// Creates an ALB result from an existing response envelope. + /// The type of content in the envelope's body. + /// The response envelope to wrap. + /// An wrapping the envelope. + public static AlbResult Create(AlbResponseEnvelope envelope) => new(envelope); +} diff --git a/src/Envelopes/MinimalLambda.Envelopes.Alb/MinimalLambda.Envelopes.Alb.csproj b/src/Envelopes/MinimalLambda.Envelopes.Alb/MinimalLambda.Envelopes.Alb.csproj index 2da41e1e..7e59109f 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.Alb/MinimalLambda.Envelopes.Alb.csproj +++ b/src/Envelopes/MinimalLambda.Envelopes.Alb/MinimalLambda.Envelopes.Alb.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Envelopes/MinimalLambda.Envelopes.Alb/README.md b/src/Envelopes/MinimalLambda.Envelopes.Alb/README.md index bceb4685..559c213d 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.Alb/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.Alb/README.md @@ -20,6 +20,7 @@ serialization: |--------------------------|-----------------------------------|---------------------------------------------| | `AlbRequestEnvelope` | `ApplicationLoadBalancerRequest` | ALB requests with deserialized body content | | `AlbResponseEnvelope` | `ApplicationLoadBalancerResponse` | ALB responses with typed body content | +| `AlbResult` | `ApplicationLoadBalancerResponse` | ALB responses with fluent API builder | ## Quick Start @@ -60,6 +61,40 @@ internal record Request(string Name); internal record Response(string Message, DateTime TimestampUtc); ``` +## Response Builder API + +The `AlbResult` class provides a fluent API for building HTTP responses. **Key benefit**: Return +multiple strongly typed models from the same handler (e.g., success vs. error responses with +different types). + +```csharp +lambda.MapHandler(([Event] AlbRequestEnvelope request) => +{ + if (string.IsNullOrEmpty(request.BodyContent?.Name)) + return AlbResult.BadRequest(new ErrorResponse("Name is required")); + + return AlbResult.Ok(new SuccessResponse($"Hello {request.BodyContent.Name}!")); +}); +``` + +Available methods: `Ok()`, `Created()`, `NoContent()`, `BadRequest()`, `Unauthorized()`, +`NotFound()`, `Conflict()`, `UnprocessableEntity()`, `InternalServerError()`, `StatusCode(int)`, +`Text(int, string)`, `Json(int, T)`. + +All methods have overloads with and without body content. Use `.Customize()` for fluent header +customization. + +> [!NOTE] +> `AlbResult` uses `AlbResponseEnvelope` internally. + +## Choosing Between Envelopes and Results + +**Use `AlbResult`** when you need to return multiple strongly typed models from the same handler +(e.g., different success and error types). Provides convenient methods for common HTTP status codes. + +**Use `AlbResponseEnvelope` directly** when you need custom serialization (e.g., XML) or want to +extend envelope base classes for custom behavior. + ## Custom Envelopes To implement custom deserialization logic, extend the appropriate base class and override the @@ -107,6 +142,17 @@ When using .NET Native AOT, register all envelope and payload types in your `Jso internal partial class SerializerContext : JsonSerializerContext; ``` +**When using `AlbResult` with multiple return types**, register each type separately: + +```csharp +[JsonSerializable(typeof(AlbRequestEnvelope))] +[JsonSerializable(typeof(AlbResult))] +[JsonSerializable(typeof(Request))] +[JsonSerializable(typeof(SuccessResponse))] +[JsonSerializable(typeof(ErrorResponse))] +internal partial class SerializerContext : JsonSerializerContext; +``` + Register the serializer and configure envelope options to use the context: ```csharp @@ -134,6 +180,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/ApiGatewayResult.cs b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/ApiGatewayResult.cs new file mode 100644 index 00000000..41c17323 --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/ApiGatewayResult.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; +using Amazon.Lambda.APIGatewayEvents; +using MinimalLambda.Options; + +namespace MinimalLambda.Envelopes.ApiGateway; + +/// +/// Represents an HTTP response for AWS Lambda functions invoked by Amazon API Gateway REST +/// API (v1). +/// +/// +/// This class wraps and provides support for response +/// envelope customization through . Use this type when returning +/// responses from Lambda proxy integrations with API Gateway REST APIs. +/// +public sealed class ApiGatewayResult : APIGatewayProxyResponse, IHttpResult +{ + [JsonIgnore] + private readonly IResponseEnvelope? _inner; + + private ApiGatewayResult(APIGatewayProxyResponse response) + { + _inner = response as IResponseEnvelope; + StatusCode = response.StatusCode; + Headers = response.Headers; + MultiValueHeaders = response.MultiValueHeaders; + Body = response.Body; + IsBase64Encoded = response.IsBase64Encoded; + } + + /// + public void PackPayload(EnvelopeOptions options) + { + if (_inner is null) + return; + + _inner.PackPayload(options); + Body = ((APIGatewayProxyResponse)_inner).Body; + } + + /// + public static ApiGatewayResult Create( + int statusCode, + T? bodyContent, + IDictionary headers, + bool isBase64Encoded + ) => + new( + new ApiGatewayResponseEnvelope + { + StatusCode = statusCode, + BodyContent = bodyContent, + Headers = headers, + IsBase64Encoded = isBase64Encoded, + } + ); + + /// Creates an API Gateway result from an existing response envelope. + /// The type of content in the envelope's body. + /// The response envelope to wrap. + /// An wrapping the envelope. + public static ApiGatewayResult Create(ApiGatewayResponseEnvelope envelope) => + new(envelope); +} diff --git a/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/ApiGatewayV2Result.cs b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/ApiGatewayV2Result.cs new file mode 100644 index 00000000..278850eb --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/ApiGatewayV2Result.cs @@ -0,0 +1,66 @@ +using System.Text.Json.Serialization; +using Amazon.Lambda.APIGatewayEvents; +using MinimalLambda.Options; + +namespace MinimalLambda.Envelopes.ApiGateway; + +/// +/// Represents an HTTP response for AWS Lambda functions invoked by Amazon API Gateway HTTP +/// API (v2). +/// +/// +/// This class wraps and provides support for +/// response envelope customization through . Use this type when +/// returning responses from Lambda proxy integrations with API Gateway HTTP APIs. +/// +public sealed class ApiGatewayV2Result + : APIGatewayHttpApiV2ProxyResponse, + IHttpResult +{ + [JsonIgnore] + private readonly IResponseEnvelope? _inner; + + private ApiGatewayV2Result(APIGatewayHttpApiV2ProxyResponse response) + { + _inner = response as IResponseEnvelope; + StatusCode = response.StatusCode; + Headers = response.Headers; + Cookies = response.Cookies; + Body = response.Body; + IsBase64Encoded = response.IsBase64Encoded; + } + + /// + public void PackPayload(EnvelopeOptions options) + { + if (_inner is null) + return; + + _inner.PackPayload(options); + Body = ((APIGatewayHttpApiV2ProxyResponse)_inner).Body; + } + + /// + public static ApiGatewayV2Result Create( + int statusCode, + T? bodyContent, + IDictionary headers, + bool isBase64Encoded + ) => + new( + new ApiGatewayV2ResponseEnvelope + { + StatusCode = statusCode, + BodyContent = bodyContent, + Headers = headers, + IsBase64Encoded = isBase64Encoded, + } + ); + + /// Creates an API Gateway v2 result from an existing response envelope. + /// The type of content in the envelope's body. + /// The response envelope to wrap. + /// An wrapping the envelope. + public static ApiGatewayV2Result Create(ApiGatewayV2ResponseEnvelope envelope) => + new(envelope); +} diff --git a/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/MinimalLambda.Envelopes.ApiGateway.csproj b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/MinimalLambda.Envelopes.ApiGateway.csproj index 98c6f1f4..f80233dc 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/MinimalLambda.Envelopes.ApiGateway.csproj +++ b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/MinimalLambda.Envelopes.ApiGateway.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md index 005090a0..33a3a71c 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md @@ -21,6 +21,8 @@ serialization: | `ApiGatewayResponseEnvelope` | `APIGatewayProxyResponse` | REST API, HTTP API payload format 1.0, or WebSocket API responses with typed body | | `ApiGatewayV2RequestEnvelope` | `APIGatewayHttpApiV2ProxyRequest` | HTTP API payload format 2.0 requests with deserialized body | | `ApiGatewayV2ResponseEnvelope` | `APIGatewayHttpApiV2ProxyResponse` | HTTP API payload format 2.0 responses with typed body | +| `ApiGatewayResult` | `APIGatewayProxyResponse` | REST/HTTP/WebSocket API responses with fluent API | +| `ApiGatewayV2Result` | `APIGatewayHttpApiV2ProxyResponse` | HTTP API v2 responses with fluent API | ## Quick Start @@ -64,6 +66,51 @@ internal record Response(string Message, DateTime TimestampUtc); For HTTP API v2, use `ApiGatewayV2RequestEnvelope` and `ApiGatewayV2ResponseEnvelope` in the same way. +## Response Builder API + +The `ApiGatewayResult` and `ApiGatewayV2Result` classes provide fluent APIs for building HTTP +responses. **Key benefit**: Return multiple strongly typed models from the same handler (e.g., +success vs. error responses with different types). + +```csharp +// REST API / HTTP API v1 / WebSocket +lambda.MapHandler(([Event] ApiGatewayRequestEnvelope request) => +{ + if (string.IsNullOrEmpty(request.BodyContent?.Name)) + return ApiGatewayResult.BadRequest(new ErrorResponse("Name is required")); + + return ApiGatewayResult.Ok(new SuccessResponse($"Hello {request.BodyContent.Name}!")); +}); + +// HTTP API v2 +lambda.MapHandler(([Event] ApiGatewayV2RequestEnvelope request) => +{ + if (string.IsNullOrEmpty(request.BodyContent?.Name)) + return ApiGatewayV2Result.BadRequest(new ErrorResponse("Name is required")); + + return ApiGatewayV2Result.Ok(new SuccessResponse($"Hello {request.BodyContent.Name}!")); +}); +``` + +Available methods: `Ok()`, `Created()`, `NoContent()`, `BadRequest()`, `Unauthorized()`, +`NotFound()`, `Conflict()`, `UnprocessableEntity()`, `InternalServerError()`, `StatusCode(int)`, +`Text(int, string)`, `Json(int, T)`. + +All methods have overloads with and without body content. Use `.Customize()` for fluent header +customization. + +> [!NOTE] +> Both result classes use their respective envelope classes internally. + +## Choosing Between Envelopes and Results + +**Use `ApiGatewayResult` / `ApiGatewayV2Result`** when you need to return multiple strongly typed +models from the same handler (e.g., different success and error types). Provides convenient methods +for common HTTP status codes. + +**Use envelope classes directly** when you need custom serialization (e.g., XML) or want to extend +envelope base classes for custom behavior. + ## Custom Envelopes To implement custom deserialization logic, extend the appropriate base class and override the @@ -111,6 +158,17 @@ When using .NET Native AOT, register all envelope and payload types in your `Jso internal partial class SerializerContext : JsonSerializerContext; ``` +**When using `ApiGatewayResult` / `ApiGatewayV2Result` with multiple return types**, register each type separately: + +```csharp +[JsonSerializable(typeof(ApiGatewayRequestEnvelope))] +[JsonSerializable(typeof(ApiGatewayResult))] +[JsonSerializable(typeof(Request))] +[JsonSerializable(typeof(SuccessResponse))] +[JsonSerializable(typeof(ErrorResponse))] +internal partial class SerializerContext : JsonSerializerContext; +``` + Register the serializer and configure envelope options to use the context: ```csharp @@ -138,6 +196,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes.CloudWatchLogs/README.md b/src/Envelopes/MinimalLambda.Envelopes.CloudWatchLogs/README.md index 2e007f85..ad7da84f 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.CloudWatchLogs/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.CloudWatchLogs/README.md @@ -127,6 +127,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes.Kafka/README.md b/src/Envelopes/MinimalLambda.Envelopes.Kafka/README.md index b4bded24..b06bc82c 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.Kafka/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.Kafka/README.md @@ -131,6 +131,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes.Kinesis/README.md b/src/Envelopes/MinimalLambda.Envelopes.Kinesis/README.md index a33b19be..a308e241 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.Kinesis/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.Kinesis/README.md @@ -122,6 +122,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes.KinesisFirehose/README.md b/src/Envelopes/MinimalLambda.Envelopes.KinesisFirehose/README.md index 1440b7a8..6f855bc2 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.KinesisFirehose/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.KinesisFirehose/README.md @@ -157,6 +157,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes.Sns/README.md b/src/Envelopes/MinimalLambda.Envelopes.Sns/README.md index bc9907a0..28cfb540 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.Sns/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.Sns/README.md @@ -109,6 +109,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes.Sqs/README.md b/src/Envelopes/MinimalLambda.Envelopes.Sqs/README.md index ecc47dfb..774535c0 100644 --- a/src/Envelopes/MinimalLambda.Envelopes.Sqs/README.md +++ b/src/Envelopes/MinimalLambda.Envelopes.Sqs/README.md @@ -153,6 +153,7 @@ source handling. | [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes**](../MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/Envelopes/MinimalLambda.Envelopes/BaseHttpResultExtensions.cs b/src/Envelopes/MinimalLambda.Envelopes/BaseHttpResultExtensions.cs new file mode 100644 index 00000000..5c7cb09c --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes/BaseHttpResultExtensions.cs @@ -0,0 +1,61 @@ +namespace MinimalLambda.Envelopes; + +/// Provides factory extension methods for creating HTTP results. +public static class BaseHttpResultExtensions +{ + extension(IHttpResult) + where THttpResult : IHttpResult + { + /// Creates an HTTP result with the specified status code. + /// The HTTP status code. + /// An HTTP result with the status code. + public static THttpResult StatusCode(int statusCode) => + THttpResult.Create(statusCode, null, new Dictionary(), false); + + /// Creates a text/plain HTTP result. + /// The HTTP status code. + /// The plain text response body. + /// An HTTP result with text/plain content type. + public static THttpResult Text(int statusCode, string body) => + THttpResult + .Create( + statusCode, + null, + new Dictionary + { + ["Content-Type"] = "text/plain; charset=utf-8", + }, + false + ) + .Customize(result => result.Body = body); + + /// Creates an application/json HTTP result. + /// The type of content to serialize. + /// The HTTP status code. + /// The content to serialize as JSON. + /// An HTTP result with application/json content type. + public static THttpResult Json(int statusCode, T bodyContent) => + THttpResult.Create( + statusCode, + bodyContent, + new Dictionary + { + ["Content-Type"] = "application/json; charset=utf-8", + }, + false + ); + } + + extension(THttpResult result) + where THttpResult : IHttpResult + { + /// Applies customizations to the result. + /// An action to customize the result properties. + /// The same instance for method chaining. + public THttpResult Customize(Action customizer) + { + customizer(result); + return result; + } + } +} diff --git a/src/Envelopes/MinimalLambda.Envelopes/HttpResultExtensions.cs b/src/Envelopes/MinimalLambda.Envelopes/HttpResultExtensions.cs new file mode 100644 index 00000000..ad815d59 --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes/HttpResultExtensions.cs @@ -0,0 +1,209 @@ +using Microsoft.AspNetCore.Http; + +namespace MinimalLambda.Envelopes; + +/// Provides convenience extension methods for common HTTP status codes. +public static class HttpResultExtensions +{ + extension(IHttpResult) + where THttpResult : IHttpResult + { + // ── 200 Ok ─────────────────────────────────────────────────────────────────────── + + /// Creates a 200 OK response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 200 result with JSON content. + public static THttpResult Ok(T bodyContent) => + BaseHttpResultExtensions.Json(StatusCodes.Status200OK, bodyContent); + + /// Creates a 200 OK response. + /// An HTTP 200 result. + public static THttpResult Ok() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status200OK); + + // ── 201 Created ────────────────────────────────────────────────────────────────── + + /// Creates a 201 Created response. + /// An HTTP 201 result. + public static THttpResult Created() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status201Created); + + /// Creates a 201 Created response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 201 result with JSON content. + public static THttpResult Created(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status201Created, + bodyContent + ); + + // ── 202 Accepted ───────────────────────────────────────────────────────────────── + + /// Creates a 202 Accepted response. + /// An HTTP 202 result. + public static THttpResult Accepted() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status202Accepted); + + /// Creates a 202 Accepted response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 202 result with JSON content. + public static THttpResult Accepted(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status202Accepted, + bodyContent + ); + + // ── 204 No Content ─────────────────────────────────────────────────────────────── + + /// Creates a 204 No Content response. + /// An HTTP 204 result. + public static THttpResult NoContent() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status204NoContent); + + // ── 301 Moved Permanently ──────────────────────────────────────────────────────── + + /// Creates a 301 Moved Permanently response. + /// An HTTP 301 result. + public static THttpResult MovedPermanently() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status301MovedPermanently); + + /// Creates a 301 Moved Permanently response with a location. + /// The URI of the redirect target. + /// An HTTP 301 result with Location header. + public static THttpResult MovedPermanently(string location) => + BaseHttpResultExtensions + .StatusCode(StatusCodes.Status301MovedPermanently) + .Customize(result => result.Headers["Location"] = location); + + // ── 302 Found ──────────────────────────────────────────────────────────────────── + + /// Creates a 302 Found response. + /// An HTTP 302 result. + public static THttpResult Found() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status302Found); + + /// Creates a 302 Found response with a location. + /// The URI of the redirect target. + /// An HTTP 302 result with Location header. + public static THttpResult Found(string location) => + BaseHttpResultExtensions + .StatusCode(StatusCodes.Status302Found) + .Customize(result => result.Headers["Location"] = location); + + // ── 400 Bad Request ────────────────────────────────────────────────────────────── + + /// Creates a 400 Bad Request response. + /// An HTTP 400 result. + public static THttpResult BadRequest() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status400BadRequest); + + /// Creates a 400 Bad Request response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 400 result with JSON content. + public static THttpResult BadRequest(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status400BadRequest, + bodyContent + ); + + // ── 401 Unauthorized ───────────────────────────────────────────────────────────── + + /// Creates a 401 Unauthorized response. + /// An HTTP 401 result. + public static THttpResult Unauthorized() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status401Unauthorized); + + // ── 403 Forbidden ──────────────────────────────────────────────────────────────── + + /// Creates a 403 Forbidden response. + /// An HTTP 403 result. + public static THttpResult Forbidden() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status403Forbidden); + + /// Creates a 403 Forbidden response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 403 result with JSON content. + public static THttpResult Forbidden(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status403Forbidden, + bodyContent + ); + + // ── 404 Not Found ──────────────────────────────────────────────────────────────── + + /// Creates a 404 Not Found response. + /// An HTTP 404 result. + public static THttpResult NotFound() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status404NotFound); + + /// Creates a 404 Not Found response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 404 result with JSON content. + public static THttpResult NotFound(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status404NotFound, + bodyContent + ); + + // ── 409 Conflict ───────────────────────────────────────────────────────────────── + + /// Creates a 409 Conflict response. + /// An HTTP 409 result. + public static THttpResult Conflict() => + BaseHttpResultExtensions.StatusCode(StatusCodes.Status409Conflict); + + /// Creates a 409 Conflict response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 409 result with JSON content. + public static THttpResult Conflict(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status409Conflict, + bodyContent + ); + + // ── 422 Unprocessable Entity ───────────────────────────────────────────────────── + + /// Creates a 422 Unprocessable Entity response. + /// An HTTP 422 result. + public static THttpResult UnprocessableEntity() => + BaseHttpResultExtensions.StatusCode( + StatusCodes.Status422UnprocessableEntity + ); + + /// Creates a 422 Unprocessable Entity response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 422 result with JSON content. + public static THttpResult UnprocessableEntity(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status422UnprocessableEntity, + bodyContent + ); + + // ── 500 Internal Server Error ──────────────────────────────────────────────────── + + /// Creates a 500 Internal Server Error response. + /// An HTTP 500 result. + public static THttpResult InternalServerError() => + BaseHttpResultExtensions.StatusCode( + StatusCodes.Status500InternalServerError + ); + + /// Creates a 500 Internal Server Error response with content. + /// The type of content to return. + /// The response content to serialize. + /// An HTTP 500 result with JSON content. + public static THttpResult InternalServerError(T bodyContent) => + BaseHttpResultExtensions.Json( + StatusCodes.Status500InternalServerError, + bodyContent + ); + } +} diff --git a/src/Envelopes/MinimalLambda.Envelopes/IHttpResult.cs b/src/Envelopes/MinimalLambda.Envelopes/IHttpResult.cs new file mode 100644 index 00000000..075042a3 --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes/IHttpResult.cs @@ -0,0 +1,36 @@ +namespace MinimalLambda.Envelopes; + +/// +/// Defines the contract for HTTP response results returned from ALB, API Gateway v1, and API +/// Gateway v2 Lambda integrations. +/// +/// The concrete implementing type for fluent method chaining. +public interface IHttpResult : IResponseEnvelope + where TSelf : IHttpResult +{ + /// Gets or sets the response body content. + public string Body { get; set; } + + /// Gets or sets the HTTP response headers. + public IDictionary Headers { get; set; } + + /// Gets or sets whether the body is base64-encoded for binary content. + public bool IsBase64Encoded { get; set; } + + /// Gets or sets the HTTP status code. + public int StatusCode { get; set; } + + /// Creates a new HTTP result instance with a strongly typed body. + /// The type of content being returned. + /// The HTTP status code. + /// The typed content to serialize into the body. + /// Response headers. + /// Whether the body is base64-encoded. + /// A new HTTP result instance. + static abstract TSelf Create( + int statusCode, + TResponse? bodyContent, + IDictionary headers, + bool isBase64Encoded + ); +} diff --git a/src/Envelopes/MinimalLambda.Envelopes/MinimalLambda.Envelopes.csproj b/src/Envelopes/MinimalLambda.Envelopes/MinimalLambda.Envelopes.csproj new file mode 100644 index 00000000..64cb54c8 --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes/MinimalLambda.Envelopes.csproj @@ -0,0 +1,24 @@ + + + net8.0;net9.0;net10.0 + preview + enable + enable + true + true + true + + MinimalLambda.Envelopes + Core components for MinimalLambda envelopes + README.md + + + + + + + + + + + diff --git a/src/Envelopes/MinimalLambda.Envelopes/README.md b/src/Envelopes/MinimalLambda.Envelopes/README.md new file mode 100644 index 00000000..b5dede00 --- /dev/null +++ b/src/Envelopes/MinimalLambda.Envelopes/README.md @@ -0,0 +1,38 @@ +# MinimalLambda.Envelopes + +Shared infrastructure and abstractions for envelope packages. + +> 📚 **[View Full Documentation](https://j-d-ha.github.io/minimal-lambda/)** + +## Overview + +This package contains shared infrastructure and common abstractions used across envelope packages in the MinimalLambda framework. + +> [!NOTE] +> This is an infrastructure package automatically referenced by other envelope packages. + +## What's Included + +- `IHttpResult` interface and extension methods for HTTP response building + +## Other Packages + +Additional packages in the minimal-lambda framework for abstractions, observability, and event source handling. + +| Package | NuGet | Downloads | +|-------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [**MinimalLambda**](../../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | +| [**MinimalLambda.Abstractions**](../../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | +| [**MinimalLambda.OpenTelemetry**](../../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Envelopes.Sqs**](../MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | +| [**MinimalLambda.Envelopes.ApiGateway**](../MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | +| [**MinimalLambda.Envelopes.Sns**](../MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | +| [**MinimalLambda.Envelopes.Kinesis**](../MinimalLambda.Envelopes.Kinesis/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Kinesis) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Kinesis.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Kinesis/) | +| [**MinimalLambda.Envelopes.KinesisFirehose**](../MinimalLambda.Envelopes.KinesisFirehose/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.KinesisFirehose) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.KinesisFirehose.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.KinesisFirehose/) | +| [**MinimalLambda.Envelopes.Kafka**](../MinimalLambda.Envelopes.Kafka/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Kafka.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Kafka) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Kafka.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Kafka/) | +| [**MinimalLambda.Envelopes.CloudWatchLogs**](../MinimalLambda.Envelopes.CloudWatchLogs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.CloudWatchLogs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.CloudWatchLogs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.CloudWatchLogs/) | +| [**MinimalLambda.Envelopes.Alb**](../MinimalLambda.Envelopes.Alb/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Alb.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Alb) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Alb.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Alb/) | + +## License + +This project is licensed under the MIT License. See [LICENSE](../../LICENSE) for details. diff --git a/src/MinimalLambda.Abstractions/README.md b/src/MinimalLambda.Abstractions/README.md index 326a587d..b0cd0bc7 100644 --- a/src/MinimalLambda.Abstractions/README.md +++ b/src/MinimalLambda.Abstractions/README.md @@ -214,6 +214,7 @@ source handling. | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | | [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | +| [**MinimalLambda.Envelopes**](../Envelopes/MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/MinimalLambda.OpenTelemetry/README.md b/src/MinimalLambda.OpenTelemetry/README.md index 75028971..68aa3be4 100644 --- a/src/MinimalLambda.OpenTelemetry/README.md +++ b/src/MinimalLambda.OpenTelemetry/README.md @@ -252,6 +252,7 @@ source handling. | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | | [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | +| [**MinimalLambda.Envelopes**](../Envelopes/MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/MinimalLambda.Testing/README.md b/src/MinimalLambda.Testing/README.md index 9c9f299a..ca70a976 100644 --- a/src/MinimalLambda.Testing/README.md +++ b/src/MinimalLambda.Testing/README.md @@ -105,6 +105,7 @@ source handling. | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | | [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | +| [**MinimalLambda.Envelopes**](../Envelopes/MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/MinimalLambda/README.md b/src/MinimalLambda/README.md index 85e20c5f..0e2d0318 100644 --- a/src/MinimalLambda/README.md +++ b/src/MinimalLambda/README.md @@ -278,6 +278,7 @@ source handling. | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | | [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | +| [**MinimalLambda.Envelopes**](../Envelopes/MinimalLambda.Envelopes/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/tasks/LocalDevTasks.yml b/tasks/LocalDevTasks.yml index b6b5d94a..ea6a1a03 100644 --- a/tasks/LocalDevTasks.yml +++ b/tasks/LocalDevTasks.yml @@ -47,7 +47,7 @@ tasks: desc: Start the AWS Lambda test tool silent: true cmds: - - dotnet lambda-test-tool start --lambda-emulator-port 5050 + - dotnet lambda-test-tool start --lambda-emulator-port 5050 --config-storage-path ./lambda_test_tool run-docs: desc: Run MKDocs server locally diff --git a/tests/MinimalLambda.Envelopes.UnitTests/AlbResultTests.cs b/tests/MinimalLambda.Envelopes.UnitTests/AlbResultTests.cs new file mode 100644 index 00000000..61df315e --- /dev/null +++ b/tests/MinimalLambda.Envelopes.UnitTests/AlbResultTests.cs @@ -0,0 +1,108 @@ +using System.Text.Json; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using AutoFixture; +using AwesomeAssertions; +using JetBrains.Annotations; +using MinimalLambda.Envelopes.Alb; +using MinimalLambda.Options; +using Xunit; + +namespace MinimalLambda.Envelopes.UnitTests; + +[TestSubject(typeof(AlbResult))] +public class AlbResultTests +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void Create_WithBodyContent_SetsPropertiesCorrectly() + { + // Arrange + var statusCode = 200; + var payload = _fixture.Create(); + var headers = new Dictionary { ["X-Custom"] = "Value" }; + var isBase64Encoded = true; + + // Act + var result = AlbResult.Create(statusCode, payload, headers, isBase64Encoded); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(statusCode); + result.Headers.Should().ContainKey("X-Custom"); + result.Headers["X-Custom"].Should().Be("Value"); + result.IsBase64Encoded.Should().Be(isBase64Encoded); + result.Body.Should().BeNull(); + } + + [Fact] + public void PackPayload_SerializesBodyContent() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + var result = AlbResult.Create(200, payload, new Dictionary(), false); + + // Act + result.PackPayload(options); + + // Assert + result.Body.Should().NotBeNull(); + var deserialized = JsonSerializer.Deserialize(result.Body); + deserialized.Should().NotBeNull(); + deserialized!.Name.Should().Be(payload.Name); + deserialized.Value.Should().Be(payload.Value); + } + + [Fact] + public void AlbResult_InheritsFromApplicationLoadBalancerResponse() + { + // Arrange + var result = AlbResult.Create(200, "test", new Dictionary(), false); + + // Act & Assert + result.Should().BeAssignableTo(); + } + + [Fact] + public void Create_WithNullBodyContent_HandlesNull() + { + // Arrange + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.Create(204, null, new Dictionary(), false); + result.PackPayload(options); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(204); + result.Body.Should().Be("null"); + } + + [Fact] + public void Create_WithEnvelope_CreatesResultCorrectly() + { + // Arrange + var payload = _fixture.Create(); + var envelope = new AlbResponseEnvelope + { + StatusCode = 201, + BodyContent = payload, + Headers = new Dictionary { ["X-Test"] = "Value" }, + IsBase64Encoded = false, + }; + + // Act + var result = AlbResult.Create(envelope); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(201); + result.Headers.Should().ContainKey("X-Test"); + result.Headers["X-Test"].Should().Be("Value"); + result.IsBase64Encoded.Should().Be(false); + } + + private record TestPayload(string Name, int Value); +} diff --git a/tests/MinimalLambda.Envelopes.UnitTests/ApiGatewayResultTests.cs b/tests/MinimalLambda.Envelopes.UnitTests/ApiGatewayResultTests.cs new file mode 100644 index 00000000..f8cd0b3e --- /dev/null +++ b/tests/MinimalLambda.Envelopes.UnitTests/ApiGatewayResultTests.cs @@ -0,0 +1,113 @@ +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using AutoFixture; +using AwesomeAssertions; +using JetBrains.Annotations; +using MinimalLambda.Envelopes.ApiGateway; +using MinimalLambda.Options; +using Xunit; + +namespace MinimalLambda.Envelopes.UnitTests; + +[TestSubject(typeof(ApiGatewayResult))] +public class ApiGatewayResultTests +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void Create_WithBodyContent_SetsPropertiesCorrectly() + { + // Arrange + var statusCode = 200; + var payload = _fixture.Create(); + var headers = new Dictionary { ["X-Custom"] = "Value" }; + var isBase64Encoded = true; + + // Act + var result = ApiGatewayResult.Create(statusCode, payload, headers, isBase64Encoded); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(statusCode); + result.Headers.Should().ContainKey("X-Custom"); + result.Headers["X-Custom"].Should().Be("Value"); + result.IsBase64Encoded.Should().Be(isBase64Encoded); + result.Body.Should().BeNull(); + } + + [Fact] + public void PackPayload_SerializesBodyContent() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + var result = ApiGatewayResult.Create(200, payload, new Dictionary(), false); + + // Act + result.PackPayload(options); + + // Assert + result.Body.Should().NotBeNull(); + var deserialized = JsonSerializer.Deserialize(result.Body); + deserialized.Should().NotBeNull(); + deserialized!.Name.Should().Be(payload.Name); + deserialized.Value.Should().Be(payload.Value); + } + + [Fact] + public void ApiGatewayResult_InheritsFromAPIGatewayProxyResponse() + { + // Arrange + var result = ApiGatewayResult.Create(200, "test", new Dictionary(), false); + + // Act & Assert + result.Should().BeAssignableTo(); + } + + [Fact] + public void Create_WithNullBodyContent_HandlesNull() + { + // Arrange + var options = new EnvelopeOptions(); + + // Act + var result = ApiGatewayResult.Create( + 204, + null, + new Dictionary(), + false + ); + result.PackPayload(options); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(204); + result.Body.Should().Be("null"); + } + + [Fact] + public void Create_WithEnvelope_CreatesResultCorrectly() + { + // Arrange + var payload = _fixture.Create(); + var envelope = new ApiGatewayResponseEnvelope + { + StatusCode = 201, + BodyContent = payload, + Headers = new Dictionary { ["X-Test"] = "Value" }, + IsBase64Encoded = false, + }; + + // Act + var result = ApiGatewayResult.Create(envelope); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(201); + result.Headers.Should().ContainKey("X-Test"); + result.Headers["X-Test"].Should().Be("Value"); + result.IsBase64Encoded.Should().Be(false); + } + + private record TestPayload(string Name, int Value); +} diff --git a/tests/MinimalLambda.Envelopes.UnitTests/ApiGatewayV2ResultTests.cs b/tests/MinimalLambda.Envelopes.UnitTests/ApiGatewayV2ResultTests.cs new file mode 100644 index 00000000..c5d1a51b --- /dev/null +++ b/tests/MinimalLambda.Envelopes.UnitTests/ApiGatewayV2ResultTests.cs @@ -0,0 +1,123 @@ +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using AutoFixture; +using AwesomeAssertions; +using JetBrains.Annotations; +using MinimalLambda.Envelopes.ApiGateway; +using MinimalLambda.Options; +using Xunit; + +namespace MinimalLambda.Envelopes.UnitTests; + +[TestSubject(typeof(ApiGatewayV2Result))] +public class ApiGatewayV2ResultTests +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void Create_WithBodyContent_SetsPropertiesCorrectly() + { + // Arrange + var statusCode = 200; + var payload = _fixture.Create(); + var headers = new Dictionary { ["X-Custom"] = "Value" }; + var isBase64Encoded = true; + + // Act + var result = ApiGatewayV2Result.Create(statusCode, payload, headers, isBase64Encoded); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(statusCode); + result.Headers.Should().ContainKey("X-Custom"); + result.Headers["X-Custom"].Should().Be("Value"); + result.IsBase64Encoded.Should().Be(isBase64Encoded); + result.Body.Should().BeNull(); + } + + [Fact] + public void PackPayload_SerializesBodyContent() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + var result = ApiGatewayV2Result.Create( + 200, + payload, + new Dictionary(), + false + ); + + // Act + result.PackPayload(options); + + // Assert + result.Body.Should().NotBeNull(); + var deserialized = JsonSerializer.Deserialize(result.Body); + deserialized.Should().NotBeNull(); + deserialized!.Name.Should().Be(payload.Name); + deserialized.Value.Should().Be(payload.Value); + } + + [Fact] + public void ApiGatewayV2Result_InheritsFromAPIGatewayHttpApiV2ProxyResponse() + { + // Arrange + var result = ApiGatewayV2Result.Create( + 200, + "test", + new Dictionary(), + false + ); + + // Act & Assert + result.Should().BeAssignableTo(); + } + + [Fact] + public void Create_WithNullBodyContent_HandlesNull() + { + // Arrange + var options = new EnvelopeOptions(); + + // Act + var result = ApiGatewayV2Result.Create( + 204, + null, + new Dictionary(), + false + ); + result.PackPayload(options); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(204); + result.Body.Should().Be("null"); + } + + [Fact] + public void Create_WithEnvelope_CreatesResultCorrectly() + { + // Arrange + var payload = _fixture.Create(); + var envelope = new ApiGatewayV2ResponseEnvelope + { + StatusCode = 201, + BodyContent = payload, + Headers = new Dictionary { ["X-Test"] = "Value" }, + IsBase64Encoded = false, + }; + + // Act + var result = ApiGatewayV2Result.Create(envelope); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(201); + result.Headers.Should().ContainKey("X-Test"); + result.Headers["X-Test"].Should().Be("Value"); + result.IsBase64Encoded.Should().Be(false); + } + + private record TestPayload(string Name, int Value); +} diff --git a/tests/MinimalLambda.Envelopes.UnitTests/BaseHttpResultExtensionsTests.cs b/tests/MinimalLambda.Envelopes.UnitTests/BaseHttpResultExtensionsTests.cs new file mode 100644 index 00000000..59d19adb --- /dev/null +++ b/tests/MinimalLambda.Envelopes.UnitTests/BaseHttpResultExtensionsTests.cs @@ -0,0 +1,91 @@ +using AutoFixture; +using AwesomeAssertions; +using JetBrains.Annotations; +using MinimalLambda.Envelopes.Alb; +using MinimalLambda.Options; +using Xunit; + +namespace MinimalLambda.Envelopes.UnitTests; + +[TestSubject(typeof(BaseHttpResultExtensions))] +public class BaseHttpResultExtensionsTests +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void StatusCode_CreatesResultWithStatusCodeAndDefaults() + { + // Arrange + var statusCode = 404; + + // Act + var result = AlbResult.StatusCode(statusCode); + + // Assert + result.StatusCode.Should().Be(statusCode); + result.Headers.Should().NotBeNull(); + result.Headers.Should().BeEmpty(); + result.Body.Should().BeNull(); + result.IsBase64Encoded.Should().BeFalse(); + } + + [Fact] + public void Text_CreatesResultWithTextPlainContentType() + { + // Arrange + var statusCode = 200; + var body = "Hello, World!"; + + // Act + var result = AlbResult.Text(statusCode, body); + + // Assert + result.StatusCode.Should().Be(statusCode); + result.Body.Should().Be(body); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("text/plain; charset=utf-8"); + } + + [Fact] + public void Json_CreatesResultWithApplicationJsonContentType() + { + // Arrange + var statusCode = 201; + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.Json(statusCode, payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(statusCode); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + result.Body.Should().Contain(payload.Value.ToString()); + } + + [Fact] + public void Customize_ModifiesPropertiesAndReturnsInstanceForChaining() + { + // Arrange + var result = AlbResult.StatusCode(200); + var customDescription = "Custom Status"; + var customHeader = "X-Custom-Header"; + + // Act + var customizedResult = result + .Customize(r => r.StatusDescription = customDescription) + .Customize(r => r.Headers[customHeader] = "CustomValue"); + + // Assert + customizedResult.Should().BeSameAs(result); + result.StatusDescription.Should().Be(customDescription); + result.Headers.Should().ContainKey(customHeader); + result.Headers[customHeader].Should().Be("CustomValue"); + } + + private record TestPayload(string Name, int Value); +} diff --git a/tests/MinimalLambda.Envelopes.UnitTests/HttpResultExtensionsTests.cs b/tests/MinimalLambda.Envelopes.UnitTests/HttpResultExtensionsTests.cs new file mode 100644 index 00000000..7ba06a2d --- /dev/null +++ b/tests/MinimalLambda.Envelopes.UnitTests/HttpResultExtensionsTests.cs @@ -0,0 +1,360 @@ +using AutoFixture; +using AwesomeAssertions; +using JetBrains.Annotations; +using MinimalLambda.Envelopes.Alb; +using MinimalLambda.Options; +using Xunit; + +namespace MinimalLambda.Envelopes.UnitTests; + +[TestSubject(typeof(HttpResultExtensions))] +public class HttpResultExtensionsTests +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void Ok_ReturnsStatus200() + { + // Act + var result = AlbResult.Ok(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(200); + } + + [Fact] + public void Ok_WithBodyContent_ReturnsStatus200WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.Ok(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(200); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void Created_ReturnsStatus201() + { + // Act + var result = AlbResult.Created(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(201); + } + + [Fact] + public void Created_WithBodyContent_ReturnsStatus201WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.Created(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(201); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void Accepted_ReturnsStatus202() + { + // Act + var result = AlbResult.Accepted(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(202); + } + + [Fact] + public void Accepted_WithBodyContent_ReturnsStatus202WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.Accepted(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(202); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void NoContent_ReturnsStatus204() + { + // Act + var result = AlbResult.NoContent(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(204); + } + + [Fact] + public void MovedPermanently_ReturnsStatus301() + { + // Act + var result = AlbResult.MovedPermanently(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(301); + } + + [Fact] + public void MovedPermanently_WithLocation_ReturnsStatus301WithLocationHeader() + { + // Arrange + var location = "https://example.com/new-location"; + + // Act + var result = AlbResult.MovedPermanently(location); + + // Assert + result.StatusCode.Should().Be(301); + result.Headers.Should().ContainKey("Location"); + result.Headers["Location"].Should().Be(location); + } + + [Fact] + public void Found_ReturnsStatus302() + { + // Act + var result = AlbResult.Found(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(302); + } + + [Fact] + public void Found_WithLocation_ReturnsStatus302WithLocationHeader() + { + // Arrange + var location = "https://example.com/temporary-location"; + + // Act + var result = AlbResult.Found(location); + + // Assert + result.StatusCode.Should().Be(302); + result.Headers.Should().ContainKey("Location"); + result.Headers["Location"].Should().Be(location); + } + + [Fact] + public void BadRequest_ReturnsStatus400() + { + // Act + var result = AlbResult.BadRequest(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(400); + } + + [Fact] + public void BadRequest_WithBodyContent_ReturnsStatus400WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.BadRequest(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(400); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void Unauthorized_ReturnsStatus401() + { + // Act + var result = AlbResult.Unauthorized(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(401); + } + + [Fact] + public void Forbidden_ReturnsStatus403() + { + // Act + var result = AlbResult.Forbidden(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(403); + } + + [Fact] + public void Forbidden_WithBodyContent_ReturnsStatus403WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.Forbidden(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(403); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void NotFound_ReturnsStatus404() + { + // Act + var result = AlbResult.NotFound(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(404); + } + + [Fact] + public void NotFound_WithBodyContent_ReturnsStatus404WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.NotFound(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(404); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void Conflict_ReturnsStatus409() + { + // Act + var result = AlbResult.Conflict(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(409); + } + + [Fact] + public void Conflict_WithBodyContent_ReturnsStatus409WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.Conflict(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(409); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void UnprocessableEntity_ReturnsStatus422() + { + // Act + var result = AlbResult.UnprocessableEntity(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(422); + } + + [Fact] + public void UnprocessableEntity_WithBodyContent_ReturnsStatus422WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.UnprocessableEntity(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(422); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + [Fact] + public void InternalServerError_ReturnsStatus500() + { + // Act + var result = AlbResult.InternalServerError(); + + // Assert + result.Should().NotBeNull(); + result.StatusCode.Should().Be(500); + } + + [Fact] + public void InternalServerError_WithBodyContent_ReturnsStatus500WithJson() + { + // Arrange + var payload = _fixture.Create(); + var options = new EnvelopeOptions(); + + // Act + var result = AlbResult.InternalServerError(payload); + result.PackPayload(options); + + // Assert + result.StatusCode.Should().Be(500); + result.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json; charset=utf-8"); + result.Body.Should().NotBeNull(); + result.Body.Should().Contain(payload.Name); + } + + private record TestPayload(string Name, int Value); +} diff --git a/tests/MinimalLambda.Envelopes.UnitTests/MinimalLambda.Envelopes.UnitTests.csproj b/tests/MinimalLambda.Envelopes.UnitTests/MinimalLambda.Envelopes.UnitTests.csproj index e6b4c2bf..12380ea6 100644 --- a/tests/MinimalLambda.Envelopes.UnitTests/MinimalLambda.Envelopes.UnitTests.csproj +++ b/tests/MinimalLambda.Envelopes.UnitTests/MinimalLambda.Envelopes.UnitTests.csproj @@ -1,6 +1,7 @@  net8.0;net9.0;net10.0 + preview enable enable false