From 25c0c3376bd26e8cc4fbf334b67892e0e7c78c5a Mon Sep 17 00:00:00 2001 From: wiowou Date: Wed, 4 Sep 2024 21:28:46 -0400 Subject: [PATCH 1/9] Support API Gateway websocket api via LambdaServer --- .../Internal/LambdaRuntimeSupportServer.cs | 41 ++++++++ .../ServiceCollectionExtensions.cs | 8 +- .../APIGatewayProxyFunction.cs | 99 ++++++++++++------- .../APIGatewayWebsocketApiProxyFunction.cs | 70 +++++++++++++ ...ewayWebsocketApiProxyFunction{TStartup}.cs | 40 ++++++++ .../Amazon.Lambda.AspNetCoreServer.Test.sln | 25 +++++ .../TestApiGatewayWebsocketApiCalls.cs | 66 +++++++++++++ ...es-post-withbody-websocketapi-request.json | 50 ++++++++++ .../Controllers/RouteKeyController.cs | 25 +++++ .../TestWebApp/WebsocketApiLambdaFunction.cs | 7 ++ 10 files changed, 397 insertions(+), 34 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json create mode 100644 Libraries/test/TestWebApp/Controllers/RouteKeyController.cs create mode 100644 Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index 05493e244..400c60cdb 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -175,4 +175,45 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) } } } + + /// + /// IServer for handlying Lambda events from an API Gateway Websocket API. + /// + public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer + { + /// + /// Create instances + /// + /// The IServiceProvider created for the ASP.NET Core application + public APIGatewayWebsocketApiLambdaRuntimeSupportServer(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + /// + /// Creates HandlerWrapper for processing events from API Gateway Websocket API + /// + /// + /// + protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider) + { + var handler = new APIGatewayWebsocketApiMinimalApi(serviceProvider).FunctionHandlerAsync; + return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer); + } + + /// + /// Create the APIGatewayWebsocketApiV2ProxyFunction passing in the ASP.NET Core application's IServiceProvider + /// + public class APIGatewayWebsocketApiMinimalApi : APIGatewayWebsocketApiProxyFunction + { + /// + /// Create instances + /// + /// The IServiceProvider created for the ASP.NET Core application + public APIGatewayWebsocketApiMinimalApi(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + } + } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 82fa10376..697becb43 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -27,7 +27,12 @@ public enum LambdaEventSource /// /// ELB Application Load Balancer /// - ApplicationLoadBalancer + ApplicationLoadBalancer, + + /// + /// API Gateway HTTP API + /// + WebsocketApi } /// @@ -106,6 +111,7 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc LambdaEventSource.HttpApi => typeof(APIGatewayHttpApiV2LambdaRuntimeSupportServer), LambdaEventSource.RestApi => typeof(APIGatewayRestApiLambdaRuntimeSupportServer), LambdaEventSource.ApplicationLoadBalancer => typeof(ApplicationLoadBalancerLambdaRuntimeSupportServer), + LambdaEventSource.WebsocketApi => typeof(APIGatewayWebsocketApiLambdaRuntimeSupportServer), _ => throw new ArgumentException($"Event source type {eventSource} unknown") }; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs index 841b3b1d5..3b897f921 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs @@ -5,6 +5,7 @@ using System.Security.Claims; using System.Text; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -151,33 +152,9 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy { var requestFeatures = (IHttpRequestFeature)features; requestFeatures.Scheme = "https"; - requestFeatures.Method = apiGatewayRequest.HttpMethod; + requestFeatures.Method = this.ParseHttpMethod(apiGatewayRequest); - string path = null; - - // Replaces {proxy+} in path, if exists - if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) && - !string.IsNullOrEmpty(apiGatewayRequest.Resource)) - { - var proxyPath = proxy; - path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath); - - // Adds all the rest of non greedy parameters in apiGateway.Resource to the path - foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy")) - { - path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value); - } - } - - if (string.IsNullOrEmpty(path)) - { - path = apiGatewayRequest.Path; - } - - if (!path.StartsWith("/")) - { - path = "/" + path; - } + string path = this.ParseHttpPath(apiGatewayRequest); var rawQueryString = Utilities.CreateQueryStringParameters( apiGatewayRequest.QueryStringParameters, apiGatewayRequest.MultiValueQueryStringParameters, true); @@ -214,13 +191,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy Utilities.SetHeadersCollection(requestFeatures.Headers, apiGatewayRequest.Headers, apiGatewayRequest.MultiValueHeaders); - if (!requestFeatures.Headers.ContainsKey("Host")) - { - var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? ""; - var stage = apiGatewayRequest?.RequestContext?.Stage ?? ""; - - requestFeatures.Headers["Host"] = $"apigateway-{apiId}-{stage}"; - } + requestFeatures.Headers = this.AddMissingRequestHeaders(apiGatewayRequest, requestFeatures.Headers); if (!string.IsNullOrEmpty(apiGatewayRequest.Body)) @@ -256,6 +227,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy { connectionFeatures.RemotePort = int.Parse(forwardedPort, CultureInfo.InvariantCulture); } + connectionFeatures.ConnectionId = apiGatewayRequest.RequestContext?.ConnectionId; // Call consumers customize method in case they want to change how API Gateway's request // was marshalled into ASP.NET Core request. @@ -335,5 +307,66 @@ protected override APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature return response; } + + /// + /// Get the http path from the request. + /// + /// + /// string + protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest) + { + string path = null; + + // Replaces {proxy+} in path, if exists + if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) && + !string.IsNullOrEmpty(apiGatewayRequest.Resource)) + { + var proxyPath = proxy; + path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath); + + // Adds all the rest of non greedy parameters in apiGateway.Resource to the path + foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy")) + { + path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value); + } + } + + if (string.IsNullOrEmpty(path)) + { + path = apiGatewayRequest.Path; + } + + if (!path.StartsWith("/")) + { + path = "/" + path; + } + return path; + } + + /// + /// Get the http method from the request. + /// + /// + /// string + protected virtual string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest) + { + return apiGatewayRequest.HttpMethod; + } + + /// + /// Add missing headers to request. + /// + /// IHeaderDictionary + protected virtual IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers) + { + if (!headers.ContainsKey("Host")) + { + var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? ""; + var stage = apiGatewayRequest?.RequestContext?.Stage ?? ""; + + headers["Host"] = $"apigateway-{apiId}-{stage}"; + } + return headers; + } } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs new file mode 100644 index 000000000..0771c7866 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs @@ -0,0 +1,70 @@ +using System; + +using Microsoft.AspNetCore.Http; + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.AspNetCoreServer.Internal; + +namespace Amazon.Lambda.AspNetCoreServer +{ + /// + /// Base class for ASP.NET Core Lambda functions that are getting request from API Gateway Websocket API V2 payload format. + /// + /// The http method is fixed as POST. Requests are handled using the RouteKey, so the same lambda should be referenced by multiple API Gateway routes for the ASP.NET Core IServer to successfully route requests. + /// + public abstract class APIGatewayWebsocketApiProxyFunction : APIGatewayProxyFunction + { + /// + /// Default constructor + /// + protected APIGatewayWebsocketApiProxyFunction() + : base() + { + + } + + /// + /// Configure when the ASP.NET Core framework will be initialized + protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode) + : base(startupMode) + { + + } + + /// + /// Constructor used by Amazon.Lambda.AspNetCoreServer.Hosting to support ASP.NET Core projects using the Minimal API style. + /// + /// + protected APIGatewayWebsocketApiProxyFunction(IServiceProvider hostedServices) + : base(hostedServices) + { + _hostServices = hostedServices; + } + + /// + /// + /// string + protected override string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest) + { + var path = "/" + Utilities.DecodeResourcePath(apiGatewayRequest.RequestContext.RouteKey); + return path; + } + + /// + /// + /// string + protected override string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest) + { + return "POST"; + } + + /// + /// IHeaderDictionary + protected override IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers) + { + headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers); + headers["Content-Type"] = "application/json"; + return headers; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs new file mode 100644 index 000000000..5d3e1b3a1 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Hosting; +using System.Diagnostics.CodeAnalysis; + +namespace Amazon.Lambda.AspNetCoreServer +{ + /// + /// APIGatewayWebsocketApiV2ProxyFunction is the base class that is implemented in a ASP.NET Core Web API. The derived class implements + /// the Init method similar to Main function in the ASP.NET Core and provides typed Startup. The function handler for + /// the Lambda function will point to this base class FunctionHandlerAsync method. + /// + /// The type containing the startup methods for the application. + public abstract class APIGatewayWebsocketApiProxyFunction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TStartup> : APIGatewayWebsocketApiProxyFunction where TStartup : class + { + /// + /// Default Constructor. The ASP.NET Core Framework will be initialized as part of the construction. + /// + protected APIGatewayWebsocketApiProxyFunction() + : base() + { + + } + + + /// + /// + /// + /// Configure when the ASP.NET Core framework will be initialized + protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode) + : base(startupMode) + { + + } + + /// + protected override void Init(IWebHostBuilder builder) + { + builder.UseStartup(); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln new file mode 100644 index 000000000..a183b9830 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.AspNetCoreServer.Test", "Amazon.Lambda.AspNetCoreServer.Test.csproj", "{AE614E81-1148-41E0-9CA7-B1F3EB34B65E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FDFB946B-997E-4736-A826-7461752C8DF4} + EndGlobalSection +EndGlobal diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs new file mode 100644 index 000000000..e24860a0f --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestUtilities; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using TestWebApp; + +using Xunit; + + + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + public class TestApiGatewayWebsocketApiCalls + { + [Fact] + public async Task TestPostWithBody() + { + var response = await this.InvokeAPIGatewayRequest("values-post-withbody-websocketapi-request.json"); + + Assert.Equal(200, response.StatusCode); + Assert.Equal("Agent, Smith", response.Body); + Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.Equal("text/plain; charset=utf-8", response.MultiValueHeaders["Content-Type"][0]); + } + + private async Task InvokeAPIGatewayRequest(string fileName, bool configureApiToReturnExceptionDetail = false) + { + return await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), GetRequestContent(fileName), configureApiToReturnExceptionDetail); + } + + private async Task InvokeAPIGatewayRequest(TestLambdaContext context, string fileName, bool configureApiToReturnExceptionDetail = false) + { + return await InvokeAPIGatewayRequestWithContent(context, GetRequestContent(fileName), configureApiToReturnExceptionDetail); + } + + private async Task InvokeAPIGatewayRequestWithContent(TestLambdaContext context, string requestContent, bool configureApiToReturnExceptionDetail = false) + { + var lambdaFunction = new TestWebApp.WebsocketLambdaFunction(); + if (configureApiToReturnExceptionDetail) + lambdaFunction.IncludeUnhandledExceptionDetailInResponse = true; + var requestStream = new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(requestContent)); + var request = new Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer().Deserialize(requestStream); + + return await lambdaFunction.FunctionHandlerAsync(request, context); + } + + private string GetRequestContent(string fileName) + { + var filePath = Path.Combine(Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), fileName); + var requestStr = File.ReadAllText(filePath); + return requestStr; + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json new file mode 100644 index 000000000..16ac75489 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json @@ -0,0 +1,50 @@ +{ + "resource": null, + "path": null, + "httpMethod": null, + "headers": null, + "queryStringParameters": null, + "stageVariables": null, + "requestContext": { + "path": null, + "accountId": null, + "resourceId": null, + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "apiKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": null, + "user": null, + "ClientCert": null + }, + "resourcePath": null, + "httpMethod": null, + "apiId": "t2yh6sjnmk", + "extendedRequestId": "amJGnGBlIBMFiTw=", + "connectionId": "amJTNfeGLAMCLCQ=", + "ConnectedAt": 1725479956267, + "DomainName": "8d611s53xy.execute-api.us-east-1.amazonaws.com", + "DomainPrefix": null, + "EventType": "MESSAGE", + "MessageId": "amJTNfeGLAMCLCQ=", + "RouteKey": "$default", + "Authorizer": null, + "OperationName": null, + "Error": null, + "IntegrationLatency": null, + "MessageDirection": "IN", + "RequestTime": "04/Sep/2024:19:59:18 +0000", + "RequestTimeEpoch": 1725479958896, + "Status": null + }, + "body": "{\"firstName\":\"Smith\",\"lastName\": \"Agent\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/Libraries/test/TestWebApp/Controllers/RouteKeyController.cs b/Libraries/test/TestWebApp/Controllers/RouteKeyController.cs new file mode 100644 index 000000000..3328efa9b --- /dev/null +++ b/Libraries/test/TestWebApp/Controllers/RouteKeyController.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + + +namespace TestWebApp.Controllers +{ + [Route("/")] + public class RouteKeyController : Controller + { + [HttpPost("$default")] + public string PostBody([FromBody] Person body) + { + return $"{body.LastName}, {body.FirstName}"; + } + + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + } +} diff --git a/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs b/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs new file mode 100644 index 000000000..105302c7f --- /dev/null +++ b/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs @@ -0,0 +1,7 @@ +using Amazon.Lambda.AspNetCoreServer; +namespace TestWebApp +{ + public class WebsocketLambdaFunction : APIGatewayWebsocketApiProxyFunction + { + } +} \ No newline at end of file From ad0ebab0a1f01fa69c99d65081406da06cc68571 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Tue, 16 Dec 2025 18:14:46 +0000 Subject: [PATCH 2/9] update permissions (#2231) (#2232) --- .github/workflows/closed-issue-message.yml | 4 ++++ .github/workflows/stale_issues.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/closed-issue-message.yml b/.github/workflows/closed-issue-message.yml index bceb11297..3c394caaa 100644 --- a/.github/workflows/closed-issue-message.yml +++ b/.github/workflows/closed-issue-message.yml @@ -2,6 +2,10 @@ name: Closed Issue Message on: issues: types: [closed] + +permissions: + issues: write + jobs: auto_comment: runs-on: ubuntu-latest diff --git a/.github/workflows/stale_issues.yml b/.github/workflows/stale_issues.yml index c81dd92b0..2eb129c00 100644 --- a/.github/workflows/stale_issues.yml +++ b/.github/workflows/stale_issues.yml @@ -5,6 +5,10 @@ on: schedule: - cron: "0 0 * * *" +permissions: + issues: write + pull-requests: write + jobs: cleanup: runs-on: ubuntu-latest From 0548eb33b4f2776baedd743054f538582f5addec Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Fri, 9 Jan 2026 12:48:34 -0500 Subject: [PATCH 3/9] Dev (#2248) * doc: update readme to point at changelog correctly (#2243) * Bump tj-actions/changed-files from 47.0.0 to 47.0.1 (#2239) Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.0 to 47.0.1. - [Release notes](https://github.com/tj-actions/changed-files/releases) - [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md) - [Commits](https://github.com/tj-actions/changed-files/compare/24d32ffd492484c1d75e0c0b894501ddb9d30d62...e0021407031f5be11a464abee9a0776171c79891) --- updated-dependencies: - dependency-name: tj-actions/changed-files dependency-version: 47.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump aws-actions/configure-aws-credentials from 5.0.0 to 5.1.1 (#2238) Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 5.0.0 to 5.1.1. - [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases) - [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/a03048d87541d1d9fcf2ecf528a4a65ba9bd7838...61815dcd50bd041e203e49132bacad1fd04d2708) --- updated-dependencies: - dependency-name: aws-actions/configure-aws-credentials dependency-version: 5.1.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add grouping for GitHub Actions updates (#2247) Group all GitHub Actions updates into a single PR. --------- Signed-off-by: dependabot[bot] Co-authored-by: Samuel Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/dependabot.yml | 8 ++++++-- .github/workflows/aws-ci.yml | 6 +++--- .github/workflows/change-file-in-pr.yml | 2 +- .github/workflows/create-release-pr.yml | 2 +- .github/workflows/sync-master-dev.yml | 2 +- README.md | 2 +- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8190c861c..5141e8016 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,3 @@ -# Set update schedule for GitHub Actions - version: 2 updates: @@ -11,3 +9,9 @@ updates: labels: - "Release Not Needed" target-branch: "dev" + # Group all github-actions updates into a single PR + groups: + all-github-actions: + applies-to: "version-updates" + patterns: + - "*" diff --git a/.github/workflows/aws-ci.yml b/.github/workflows/aws-ci.yml index fddddbd66..582ce5eed 100644 --- a/.github/workflows/aws-ci.yml +++ b/.github/workflows/aws-ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Configure Load Balancer Credentials - uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v4 + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 #v4 with: role-to-assume: ${{ secrets.CI_MAIN_TESTING_ACCOUNT_ROLE_ARN }} role-duration-seconds: 7200 @@ -29,7 +29,7 @@ jobs: $roleArn=$(cat ./response.json) "roleArn=$($roleArn -replace '"', '')" >> $env:GITHUB_OUTPUT - name: Configure Test Runner Credentials - uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v4 + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 #v4 with: role-to-assume: ${{ steps.lambda.outputs.roleArn }} role-duration-seconds: 7200 @@ -41,7 +41,7 @@ jobs: project-name: ${{ secrets.CI_TESTING_CODE_BUILD_PROJECT_NAME }} - name: Configure Test Sweeper Lambda Credentials if: always() - uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v4 + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 #v4 with: role-to-assume: ${{ steps.lambda.outputs.roleArn }} role-duration-seconds: 7200 diff --git a/.github/workflows/change-file-in-pr.yml b/.github/workflows/change-file-in-pr.yml index 944b3bc94..24c9477bd 100644 --- a/.github/workflows/change-file-in-pr.yml +++ b/.github/workflows/change-file-in-pr.yml @@ -16,7 +16,7 @@ jobs: - name: Get List of Changed Files id: changed-files - uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 #v45 + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 #v45 - name: Check for Change File(s) in .autover/changes/ run: | diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 952a75d14..27a6ed751 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -25,7 +25,7 @@ jobs: steps: # Assume an AWS Role that provides access to the Access Token - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v5.0.0 + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 #v5.1.1 with: role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} aws-region: us-west-2 diff --git a/.github/workflows/sync-master-dev.yml b/.github/workflows/sync-master-dev.yml index 356fde29c..1eec374f3 100644 --- a/.github/workflows/sync-master-dev.yml +++ b/.github/workflows/sync-master-dev.yml @@ -26,7 +26,7 @@ jobs: steps: # Assume an AWS Role that provides access to the Access Token - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #v5.0.0 + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 #v5.1.1 with: role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} aws-region: us-west-2 diff --git a/README.md b/README.md index 37871d167..405e952a5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Repository for the AWS NuGet packages and Blueprints to support writing AWS Lambda functions using .NET Core. -For a history of releases view the [release change log](RELEASE.CHANGELOG.md) +For a history of releases view the [release change log](CHANGELOG.md) ## Table of Contents - [AWS Lambda for .NET Core ![Gitter](https://gitter.im/aws/aws-lambda-dotnet?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)](#aws-lambda-for-net-core-img-srchttpsbadgesgitterimjoin20chatsvg-altgitter) From 40cdd7f656d8de63fbc87c1503ff2200f9d76d35 Mon Sep 17 00:00:00 2001 From: bk-play Date: Tue, 12 May 2026 20:54:46 -0400 Subject: [PATCH 4/9] pr fixes: api gateway websocket api --- .../Internal/LambdaRuntimeSupportServer.cs | 2 +- .../ServiceCollectionExtensions.cs | 2 +- .../APIGatewayWebsocketApiProxyFunction.cs | 5 +++- ...ewayWebsocketApiProxyFunction{TStartup}.cs | 2 +- .../TestApiGatewayWebsocketApiCalls.cs | 8 ------ ...es-post-withbody-websocketapi-request.json | 28 +++++++++---------- 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index 400c60cdb..a59933013 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -177,7 +177,7 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) } /// - /// IServer for handlying Lambda events from an API Gateway Websocket API. + /// IServer for handling Lambda events from an API Gateway Websocket API. /// public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer { diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 697becb43..407e358e5 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -30,7 +30,7 @@ public enum LambdaEventSource ApplicationLoadBalancer, /// - /// API Gateway HTTP API + /// API Gateway WebSocket API /// WebsocketApi } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs index 0771c7866..cf00d91dd 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs @@ -63,7 +63,10 @@ protected override string ParseHttpMethod(APIGatewayProxyRequest apiGatewayReque protected override IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers) { headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers); - headers["Content-Type"] = "application/json"; + if (!headers.ContainsKey("Content-Type")) + { + headers["Content-Type"] = "application/json"; + } return headers; } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs index 5d3e1b3a1..b99397879 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs @@ -4,7 +4,7 @@ namespace Amazon.Lambda.AspNetCoreServer { /// - /// APIGatewayWebsocketApiV2ProxyFunction is the base class that is implemented in a ASP.NET Core Web API. The derived class implements + /// APIGatewayWebsocketApiProxyFunction is the base class that is implemented in an ASP.NET Core Web API. The derived class implements /// the Init method similar to Main function in the ASP.NET Core and provides typed Startup. The function handler for /// the Lambda function will point to this base class FunctionHandlerAsync method. /// diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs index e24860a0f..937a941af 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs @@ -1,19 +1,11 @@ using System; -using System.Collections.Generic; using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; using System.Reflection; -using System.Text; using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.TestUtilities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - using TestWebApp; using Xunit; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json index 16ac75489..20760653c 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json @@ -30,20 +30,20 @@ "apiId": "t2yh6sjnmk", "extendedRequestId": "amJGnGBlIBMFiTw=", "connectionId": "amJTNfeGLAMCLCQ=", - "ConnectedAt": 1725479956267, - "DomainName": "8d611s53xy.execute-api.us-east-1.amazonaws.com", - "DomainPrefix": null, - "EventType": "MESSAGE", - "MessageId": "amJTNfeGLAMCLCQ=", - "RouteKey": "$default", - "Authorizer": null, - "OperationName": null, - "Error": null, - "IntegrationLatency": null, - "MessageDirection": "IN", - "RequestTime": "04/Sep/2024:19:59:18 +0000", - "RequestTimeEpoch": 1725479958896, - "Status": null + "connectedAt": 1725479956267, + "domainName": "8d611s53xy.execute-api.us-east-1.amazonaws.com", + "domainPrefix": null, + "eventType": "MESSAGE", + "messageId": "amJTNfeGLAMCLCQ=", + "routeKey": "$default", + "authorizer": null, + "operationName": null, + "error": null, + "integrationLatency": null, + "messageDirection": "IN", + "requestTime": "04/Sep/2024:19:59:18 +0000", + "requestTimeEpoch": 1725479958896, + "status": null }, "body": "{\"firstName\":\"Smith\",\"lastName\": \"Agent\"}", "isBase64Encoded": false From 0fc8136fc9bd6b22c7602d3593eaafb752d2f863 Mon Sep 17 00:00:00 2001 From: bk-play Date: Tue, 12 May 2026 21:45:51 -0400 Subject: [PATCH 5/9] fix lambda runtime support server --- .../Internal/LambdaRuntimeSupportServer.cs | 113 ++++++++++++++++-- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index 0a89ec1cd..b094c83ff 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -464,10 +464,9 @@ protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCore } } } -<<<<<<< HEAD /// - /// IServer for handling Lambda events from an API Gateway Websocket API. + /// IServer for handlying Lambda events from an API Gateway Websocket API. /// public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer { @@ -481,32 +480,124 @@ public APIGatewayWebsocketApiLambdaRuntimeSupportServer(IServiceProvider service } /// - /// Creates HandlerWrapper for processing events from API Gateway Websocket API + /// Creates HandlerWrapper for processing events from API Gateway HTTP API /// /// /// protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider) { - var handler = new APIGatewayWebsocketApiMinimalApi(serviceProvider).FunctionHandlerAsync; - return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer); + var handler = new APIGatewayWebsocketMinimalApi(serviceProvider); +#pragma warning disable CA2252 + var hostingOptions = serviceProvider.GetService(); + handler.EnableResponseStreaming = hostingOptions?.EnableResponseStreaming ?? false; +#pragma warning restore CA2252 + Func> bufferedHandler = handler.FunctionHandlerAsync; + return HandlerWrapper.GetHandlerWrapper(bufferedHandler, Serializer); } /// - /// Create the APIGatewayWebsocketApiV2ProxyFunction passing in the ASP.NET Core application's IServiceProvider + /// Create the APIGatewayWebsocketApiProxyFunction passing in the ASP.NET Core application's IServiceProvider /// - public class APIGatewayWebsocketApiMinimalApi : APIGatewayWebsocketApiProxyFunction + public class APIGatewayWebsocketMinimalApi : APIGatewayHttpApiV2ProxyFunction { + private readonly IEnumerable _beforeSnapshotRequestsCollectors; + private readonly HostingOptions? _hostingOptions; + /// /// Create instances /// /// The IServiceProvider created for the ASP.NET Core application - public APIGatewayWebsocketApiMinimalApi(IServiceProvider serviceProvider) + public APIGatewayWebsocketMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { + _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); + + // Retrieve HostingOptions from service provider (may be null for backward compatibility) + _hostingOptions = serviceProvider.GetService(); + + // Apply configuration from HostingOptions if available + if (_hostingOptions != null) + { + // Apply binary response configuration + foreach (var kvp in _hostingOptions.ContentTypeEncodings) + { + RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); + } + + foreach (var kvp in _hostingOptions.ContentEncodingEncodings) + { + RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); + } + + DefaultResponseContentEncoding = _hostingOptions.DefaultResponseContentEncoding; + + // Apply exception handling configuration + IncludeUnhandledExceptionDetailInResponse = _hostingOptions.IncludeUnhandledExceptionDetailInResponse; + } + } + + /// + protected override IEnumerable GetBeforeSnapshotRequests() + { + foreach (var collector in _beforeSnapshotRequestsCollectors) + if (collector.Request != null) + yield return collector.Request; + } + + /// + protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + } + + /// + protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse lambdaResponse, ILambdaContext lambdaContext) + { + base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + } + + /// + protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + /// + protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + } + + /// + protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + /// + protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + // Note: LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved by the base implementation + _hostingOptions?.PostMarshallItemsFeature?.Invoke(aspNetCoreItemFeature, lambdaRequest, lambdaContext); } } } } -======= -} ->>>>>>> master From 5ad44ab23a79e813b8bc03730d51af7224f83e3a Mon Sep 17 00:00:00 2001 From: bk-play Date: Tue, 12 May 2026 22:44:47 -0400 Subject: [PATCH 6/9] simplify calls --- .../TestApiGatewayWebsocketApiCalls.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs index 937a941af..07d0551da 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs @@ -19,7 +19,7 @@ public class TestApiGatewayWebsocketApiCalls [Fact] public async Task TestPostWithBody() { - var response = await this.InvokeAPIGatewayRequest("values-post-withbody-websocketapi-request.json"); + var response = await InvokeAPIGatewayRequest("values-post-withbody-websocketapi-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("Agent, Smith", response.Body); @@ -50,7 +50,7 @@ private async Task InvokeAPIGatewayRequestWithContent(T private string GetRequestContent(string fileName) { - var filePath = Path.Combine(Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), fileName); + var filePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), fileName); var requestStr = File.ReadAllText(filePath); return requestStr; } From acd15d70d759d669a215ffa602b8a8c54507c58c Mon Sep 17 00:00:00 2001 From: bk-play Date: Tue, 12 May 2026 20:54:46 -0400 Subject: [PATCH 7/9] pr fixes: api gateway websocket api --- .../Internal/LambdaRuntimeSupportServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index b094c83ff..1e8cacb08 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -466,7 +466,7 @@ protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCore } /// - /// IServer for handlying Lambda events from an API Gateway Websocket API. + /// IServer for handling Lambda events from an API Gateway Websocket API. /// public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer { From 26b9cd00f145fb9596bd6e3aecb62673fdae2e5b Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 14 May 2026 21:21:12 -0400 Subject: [PATCH 8/9] Fix websocket Hosting wiring and simplify MinimalApi The websocket runtime-support server was wired to the v2 APIGatewayHttpApiV2ProxyFunction handler with v2 request/response types, so AddAWSLambdaHosting(LambdaEventSource.WebsocketApi) never ran the websocket-specific path/method/header overrides and dropped fields like ConnectionId. Switch it to the v1 APIGatewayWebsocketApiProxyFunction flow and have the websocket MinimalApi inherit from the REST one, overriding only the three websocket virtuals. Tighten doc comments on the new types and on ParseHttpPath/ParseHttpMethod/AddMissingRequestHeaders, and remove the stray test .sln (the project builds via the existing .csproj from Libraries.sln). --- .../Internal/LambdaRuntimeSupportServer.cs | 104 +++--------------- .../APIGatewayProxyFunction.cs | 17 +-- .../APIGatewayWebsocketApiProxyFunction.cs | 43 ++++---- ...ewayWebsocketApiProxyFunction{TStartup}.cs | 14 +-- .../Amazon.Lambda.AspNetCoreServer.Test.sln | 25 ----- 5 files changed, 55 insertions(+), 148 deletions(-) delete mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index 1e8cacb08..b99d62a86 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -468,7 +468,7 @@ protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCore /// /// IServer for handling Lambda events from an API Gateway Websocket API. /// - public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer + public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : APIGatewayRestApiLambdaRuntimeSupportServer { /// /// Create instances @@ -480,7 +480,7 @@ public APIGatewayWebsocketApiLambdaRuntimeSupportServer(IServiceProvider service } /// - /// Creates HandlerWrapper for processing events from API Gateway HTTP API + /// Creates HandlerWrapper for processing events from API Gateway Websocket API /// /// /// @@ -491,18 +491,17 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP var hostingOptions = serviceProvider.GetService(); handler.EnableResponseStreaming = hostingOptions?.EnableResponseStreaming ?? false; #pragma warning restore CA2252 - Func> bufferedHandler = handler.FunctionHandlerAsync; + Func> bufferedHandler = handler.FunctionHandlerAsync; return HandlerWrapper.GetHandlerWrapper(bufferedHandler, Serializer); } /// - /// Create the APIGatewayWebsocketApiProxyFunction passing in the ASP.NET Core application's IServiceProvider + /// MinimalApi variant of APIGatewayWebsocketApiProxyFunction. Reuses the REST API MinimalApi plumbing + /// (snapshot collectors, hosting options, PostMarshall callbacks) and applies the websocket-specific + /// request transformations on top. /// - public class APIGatewayWebsocketMinimalApi : APIGatewayHttpApiV2ProxyFunction + public class APIGatewayWebsocketMinimalApi : APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi { - private readonly IEnumerable _beforeSnapshotRequestsCollectors; - private readonly HostingOptions? _hostingOptions; - /// /// Create instances /// @@ -510,93 +509,24 @@ public class APIGatewayWebsocketMinimalApi : APIGatewayHttpApiV2ProxyFunction public APIGatewayWebsocketMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { - _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); - - // Retrieve HostingOptions from service provider (may be null for backward compatibility) - _hostingOptions = serviceProvider.GetService(); - - // Apply configuration from HostingOptions if available - if (_hostingOptions != null) - { - // Apply binary response configuration - foreach (var kvp in _hostingOptions.ContentTypeEncodings) - { - RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); - } - - foreach (var kvp in _hostingOptions.ContentEncodingEncodings) - { - RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); - } - - DefaultResponseContentEncoding = _hostingOptions.DefaultResponseContentEncoding; - - // Apply exception handling configuration - IncludeUnhandledExceptionDetailInResponse = _hostingOptions.IncludeUnhandledExceptionDetailInResponse; - } } /// - protected override IEnumerable GetBeforeSnapshotRequests() - { - foreach (var collector in _beforeSnapshotRequestsCollectors) - if (collector.Request != null) - yield return collector.Request; - } + protected override string ParseHttpPath(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest) + => "/" + System.Net.WebUtility.UrlDecode(apiGatewayRequest.RequestContext.RouteKey ?? string.Empty); /// - protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) - { - base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); - - // Invoke configured callback if available - _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); - } + protected override string ParseHttpMethod(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest) => "POST"; /// - protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse lambdaResponse, ILambdaContext lambdaContext) + protected override Microsoft.AspNetCore.Http.IHeaderDictionary AddMissingRequestHeaders(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest, Microsoft.AspNetCore.Http.IHeaderDictionary headers) { - base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); - - // Invoke configured callback if available - _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); - } - - /// - protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) - { - base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); - - // Invoke configured callback if available - _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); - } - - /// - protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) - { - base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); - - // Invoke configured callback if available - _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); - } - - /// - protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) - { - base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); - - // Invoke configured callback if available - _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); - } - - /// - protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) - { - base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); - - // Invoke configured callback if available - // Note: LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved by the base implementation - _hostingOptions?.PostMarshallItemsFeature?.Invoke(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers); + if (!headers.ContainsKey("Content-Type")) + { + headers["Content-Type"] = "application/json"; + } + return headers; } } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs index 898d69c16..1398239de 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs @@ -354,10 +354,10 @@ protected override APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature } /// - /// Get the http path from the request. + /// Determines the path that should be assigned to for this request. + /// The default implementation honors {proxy+} resource templates and falls back to . + /// Subclasses can override to derive the path from a different source (e.g. websocket route keys). /// - /// - /// string protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest) { string path = null; @@ -389,19 +389,20 @@ protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest) } /// - /// Get the http method from the request. + /// Determines the HTTP method that should be assigned to . + /// The default returns ; subclasses can override to force + /// a fixed method (e.g. websocket events are always exposed as POST). /// - /// - /// string protected virtual string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest) { return apiGatewayRequest.HttpMethod; } /// - /// Add missing headers to request. + /// Adds any headers that API Gateway did not include but ASP.NET Core needs in order to route or bind the request. + /// The default implementation adds a synthesized Host header. Subclasses can override to add additional defaults + /// (e.g. a default Content-Type for websocket payloads). /// - /// IHeaderDictionary protected virtual IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers) { if (!headers.ContainsKey("Host")) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs index cf00d91dd..2c5024c38 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs @@ -8,58 +8,63 @@ namespace Amazon.Lambda.AspNetCoreServer { /// - /// Base class for ASP.NET Core Lambda functions that are getting request from API Gateway Websocket API V2 payload format. + /// Base class for ASP.NET Core Lambda functions invoked by API Gateway Websocket APIs. /// - /// The http method is fixed as POST. Requests are handled using the RouteKey, so the same lambda should be referenced by multiple API Gateway routes for the ASP.NET Core IServer to successfully route requests. + /// Websocket events are surfaced as POST requests whose path is the RouteKey, so the same Lambda + /// should be referenced by every websocket route that has a matching ASP.NET Core controller route + /// (e.g. [HttpPost("$default")], [HttpPost("$connect")]) for the ASP.NET Core IServer + /// to successfully dispatch requests. /// public abstract class APIGatewayWebsocketApiProxyFunction : APIGatewayProxyFunction { /// - /// Default constructor + /// Default constructor. The ASP.NET Core framework is initialized as part of construction. /// protected APIGatewayWebsocketApiProxyFunction() : base() { - } - /// - /// Configure when the ASP.NET Core framework will be initialized + /// + /// Constructor that lets the caller defer ASP.NET Core framework initialization until the first request. + /// + /// Configures when the ASP.NET Core framework is initialized. protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode) : base(startupMode) { - } /// /// Constructor used by Amazon.Lambda.AspNetCoreServer.Hosting to support ASP.NET Core projects using the Minimal API style. /// - /// + /// The service provider built by the ASP.NET Core host. protected APIGatewayWebsocketApiProxyFunction(IServiceProvider hostedServices) : base(hostedServices) { - _hostServices = hostedServices; } - /// - /// - /// string + /// + /// Maps the websocket event to a request path of /{RouteKey} so ASP.NET Core can dispatch to a controller + /// route declared with [HttpPost("{RouteKey}")]. + /// protected override string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest) { - var path = "/" + Utilities.DecodeResourcePath(apiGatewayRequest.RequestContext.RouteKey); - return path; + return "/" + Utilities.DecodeResourcePath(apiGatewayRequest.RequestContext.RouteKey); } - /// - /// - /// string + /// + /// Always returns POST for websocket events. Combined with , this lets the same + /// Lambda route every websocket event into an ASP.NET Core controller action. + /// protected override string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest) { return "POST"; } - /// - /// IHeaderDictionary + /// + /// Adds a default Content-Type of application/json when API Gateway did not supply one. + /// Websocket message payloads are typically JSON, but the gateway does not set headers automatically. + /// protected override IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers) { headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers); diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs index b99397879..c264483c4 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs @@ -4,31 +4,27 @@ namespace Amazon.Lambda.AspNetCoreServer { /// - /// APIGatewayWebsocketApiProxyFunction is the base class that is implemented in an ASP.NET Core Web API. The derived class implements - /// the Init method similar to Main function in the ASP.NET Core and provides typed Startup. The function handler for - /// the Lambda function will point to this base class FunctionHandlerAsync method. + /// Strongly-typed variant of that wires up an ASP.NET Core Startup class. + /// The Lambda function handler should point at the inherited FunctionHandlerAsync method. /// /// The type containing the startup methods for the application. public abstract class APIGatewayWebsocketApiProxyFunction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TStartup> : APIGatewayWebsocketApiProxyFunction where TStartup : class { /// - /// Default Constructor. The ASP.NET Core Framework will be initialized as part of the construction. + /// Default constructor. The ASP.NET Core framework is initialized as part of construction. /// protected APIGatewayWebsocketApiProxyFunction() : base() { - } - /// - /// + /// Constructor that lets the caller defer ASP.NET Core framework initialization until the first request. /// - /// Configure when the ASP.NET Core framework will be initialized + /// Configures when the ASP.NET Core framework is initialized. protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode) : base(startupMode) { - } /// diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln deleted file mode 100644 index a183b9830..000000000 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.AspNetCoreServer.Test", "Amazon.Lambda.AspNetCoreServer.Test.csproj", "{AE614E81-1148-41E0-9CA7-B1F3EB34B65E}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FDFB946B-997E-4736-A826-7461752C8DF4} - EndGlobalSection -EndGlobal From 05ae11cc758831486b9fa650655c1ec6c9f90595 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 14 May 2026 21:28:53 -0400 Subject: [PATCH 9/9] Add autover change file for websocket support Records the public-API addition of APIGatewayWebsocketApiProxyFunction + LambdaEventSource.WebsocketApi as a Minor bump on Amazon.Lambda.AspNetCoreServer and Amazon.Lambda.AspNetCoreServer.Hosting. --- .../apigateway-websocket-aspnetcore.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .autover/changes/apigateway-websocket-aspnetcore.json diff --git a/.autover/changes/apigateway-websocket-aspnetcore.json b/.autover/changes/apigateway-websocket-aspnetcore.json new file mode 100644 index 000000000..2c8bc7952 --- /dev/null +++ b/.autover/changes/apigateway-websocket-aspnetcore.json @@ -0,0 +1,19 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.AspNetCoreServer", + "Type": "Minor", + "ChangelogMessages": [ + "Add APIGatewayWebsocketApiProxyFunction (and TStartup variant) so API Gateway WebSocket APIs can be hosted via LambdaServer (DI, controllers). WebSocket events are dispatched as POST requests whose path is the RouteKey, allowing controller actions like [HttpPost(\"$default\")] to handle them.", + "Expose ParseHttpPath, ParseHttpMethod, and AddMissingRequestHeaders as protected virtual hooks on APIGatewayProxyFunction so subclasses can customize how the API Gateway request is mapped onto the ASP.NET Core request feature." + ] + }, + { + "Name": "Amazon.Lambda.AspNetCoreServer.Hosting", + "Type": "Minor", + "ChangelogMessages": [ + "Add LambdaEventSource.WebsocketApi so AddAWSLambdaHosting can wire ASP.NET Core minimal APIs and controllers up to API Gateway WebSocket events." + ] + } + ] +}