From 35893ddbf8069c90d304bc3d0944289def5fec8e Mon Sep 17 00:00:00 2001 From: philip pittle Date: Thu, 6 Mar 2025 11:37:25 -0800 Subject: [PATCH 01/17] Add AddAWSLambdaBeforeSnapshotRequest to support warming up the asp.net/lambda pipelines automatically during BeforeSnapshot callback. --- .../Internal/LambdaRuntimeSupportServer.cs | 23 +++- ...tartExecuteRequestsBeforeSnapshotHelper.cs | 120 ++++++++++++++++++ .../ServiceCollectionExtensions.cs | 45 ++++++- .../Helpers/SnapstartHelperLambdaRequests.cs | 56 ++++++++ 4 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.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..597c1877b 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -1,9 +1,11 @@ -using Amazon.Lambda.AspNetCoreServer.Internal; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; +using Amazon.Lambda.AspNetCoreServer.Internal; using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.RuntimeSupport.Helpers; using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal @@ -16,7 +18,9 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal /// public abstract class LambdaRuntimeSupportServer : LambdaServer { - IServiceProvider _serviceProvider; + private readonly IServiceProvider _serviceProvider; + private readonly LambdaSnapstartExecuteRequestsBeforeSnapshotHelper _snapstartInitHelper; + internal ILambdaSerializer Serializer; /// @@ -26,6 +30,7 @@ public abstract class LambdaRuntimeSupportServer : LambdaServer public LambdaRuntimeSupportServer(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; + _snapstartInitHelper = _serviceProvider.GetRequiredService(); Serializer = serviceProvider.GetRequiredService(); } @@ -36,11 +41,19 @@ public LambdaRuntimeSupportServer(IServiceProvider serviceProvider) /// /// /// + [RequiresUnreferencedCode("_snapstartInitHelper Serializes objects to Json")] public override Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) { base.StartAsync(application, cancellationToken); var handlerWrapper = CreateHandlerWrapper(_serviceProvider); + + #if NET8_0_OR_GREATER + + _snapstartInitHelper.RegisterInitializerRequests(handlerWrapper); + + #endif + var bootStrap = new LambdaBootstrap(handlerWrapper); return bootStrap.RunAsync(); } @@ -175,4 +188,4 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) } } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs new file mode 100644 index 000000000..46395b8d6 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -0,0 +1,120 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.RuntimeSupport.Helpers; +using Microsoft.Extensions.DependencyInjection; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; + +/// +/// Contains the plumbing to register a user provided inside +/// . +/// The function is meant to initialize the asp.net and lambda pipelines during +/// and improve the +/// performance gains offered by SnapStart. +/// +/// It works by construction a specialized that will intercept requests +/// and saved them inside . +/// +/// Intercepted requests are then be processed later by +/// which will route them correctly through a simulated asp.net/lambda pipeline. +/// +internal class LambdaSnapstartExecuteRequestsBeforeSnapshotHelper +{ + private readonly LambdaEventSource _lambdaEventSource; + + public LambdaSnapstartExecuteRequestsBeforeSnapshotHelper(LambdaEventSource lambdaEventSource) + { + _lambdaEventSource = lambdaEventSource; + } + + /// + [RequiresUnreferencedCode("Serializes object to json")] + public void RegisterInitializerRequests(HandlerWrapper handlerWrapper) + { + #if NET8_0_OR_GREATER + + Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => + { + // Construct specialized HttpClient that will intercept requests and saved them inside + // LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequests. + // + // They will be processed later by SnapstartHelperLambdaRequests.ExecuteSnapstartInitRequests which will + // route them correctly through a simulated lambda pipeline. + var messageHandlerThatCollectsRequests = new LambdaSnapstartInitializerHttpMessageHandler(_lambdaEventSource); + + var httpClientThatCollectsRequests = new HttpClient(messageHandlerThatCollectsRequests); + httpClientThatCollectsRequests.BaseAddress = LambdaSnapstartInitializerHttpMessageHandler.BaseUri; + + // "Invoke" each registered request function. Requests will be captured inside. + // LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequests. + await Registrar.Execute(httpClientThatCollectsRequests); + + // Request are now in CapturedHttpRequests. Serialize each one into a json object + // and execute the request through the lambda pipeline (ie handlerWrapper). + foreach (var req in LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequests) + { + var json = JsonSerializer.Serialize(req); + + // TODO - inline + await SnapstartHelperLambdaRequests.ExecuteSnapstartInitRequests(json, times: 5, handlerWrapper); + } + }); + + #endif + } + + /// + internal static BeforeSnapstartRequestRegistrar Registrar = new(); + + internal class BeforeSnapstartRequestRegistrar + { + private List> beforeSnapstartFuncs = new(); + + public void Register(Func beforeSnapstartRequest) + { + beforeSnapstartFuncs.Add(beforeSnapstartRequest); + } + + internal async Task Execute(HttpClient client) + { + foreach (var f in beforeSnapstartFuncs) + await f(client); + } + } + + private class LambdaSnapstartInitializerHttpMessageHandler : HttpMessageHandler + { + private LambdaEventSource _lambdaEventSource; + + public static Uri BaseUri { get; } = new Uri("http://localhost"); + + public static List CapturedHttpRequests { get; } = new(); + + public LambdaSnapstartInitializerHttpMessageHandler(LambdaEventSource lambdaEventSource) + { + _lambdaEventSource = lambdaEventSource; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Copy request to correct request, ie APIGatewayProxyRequest + + // TODO - IMPLEMENT + var translatedRequest = new APIGatewayProxyRequest + { + Path = request.RequestUri.MakeRelativeUri(BaseUri).ToString(), + HttpMethod = request.Method.ToString() + }; + + CapturedHttpRequests.Add(translatedRequest); + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 82fa10376..daee9866e 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.AspNetCoreServer.Hosting; +using Amazon.Lambda.AspNetCoreServer.Hosting; using Amazon.Lambda.AspNetCoreServer.Internal; using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; using Amazon.Lambda.Core; @@ -88,6 +88,46 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser return services; } + /// + /// Adds a function meant to initialize the asp.net and lambda pipelines during + /// improving the performance gains offered by SnapStart. + /// + /// Use the passed to invoke one or more Routes in your lambda function. + /// Be aware that this will invoke your applications function handler code + /// multiple times. It uses a mock which may not be fully populated. + /// + /// This method automatically registers with . + /// + /// If SnapStart is not enabled, then this method is ignored and is never invoked. + /// + /// Example: + /// + /// + /// + /// { + /// await httpClient.GetAsync($"/test"); + /// }); + /// + /// var app = builder.Build(); + /// + /// app.MapGet($"/test", () => "Success"); + /// + /// app.Run(); + /// ]]> + /// + /// + public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, Func beforeSnapStartRequest) + { + LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.Registrar.Register(beforeSnapStartRequest); + + return services; + } + private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSource eventSource, Action? configure, out HostingOptions? hostingOptions) { hostingOptions = null; @@ -111,6 +151,9 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc Utilities.EnsureLambdaServerRegistered(services, serverType); + // register a LambdaSnapStartInitializerHttpMessageHandler + services.AddSingleton(new LambdaSnapstartExecuteRequestsBeforeSnapshotHelper(eventSource)); + return true; } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.cs new file mode 100644 index 000000000..05a0c3b62 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport.Helpers +{ + /// + /// should not be public + /// this is public to allow LambdaRuntimeSupportServer access, + /// otherwise it would need access to and + /// + /// + /// TODO - inline to LambdaSnapstartExecuteRequestsBeforeSnapshotHelper + /// + public static class SnapstartHelperLambdaRequests + { + private static InternalLogger _logger = InternalLogger.GetDefaultLogger(); + + private static readonly RuntimeApiHeaders _fakeRuntimeApiHeaders = new(new Dictionary> + { + { RuntimeApiHeaders.HeaderAwsRequestId, new List() }, + { RuntimeApiHeaders.HeaderTraceId, new List() }, + { RuntimeApiHeaders.HeaderClientContext, new List() }, + { RuntimeApiHeaders.HeaderCognitoIdentity, new List() }, + { RuntimeApiHeaders.HeaderDeadlineMs, new List() }, + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List() }, + }); + + public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int times, HandlerWrapper handlerWrapper) + { + var dummyRequest = new InvocationRequest + { + InputStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonRequest)), + LambdaContext = new LambdaContext( + _fakeRuntimeApiHeaders, + new LambdaEnvironment(), + new SimpleLoggerWriter()) + }; + + for (var i = 0; i < times; i++) + { + try + { + _ = await handlerWrapper.Handler.Invoke(dummyRequest); + } + catch (Exception e) + { + Console.WriteLine("StartAsync: " + e.Message + e.StackTrace); + _logger.LogError(e, "StartAsync: Custom Warmup Failure: " + e.Message + e.StackTrace); + } + } + } + } +} From 062273083e7ac7049d304994f2f43ae9ebeb0e65 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Wed, 12 Mar 2025 11:08:22 -0700 Subject: [PATCH 02/17] inline SnapstartHelperLambdaRequest --- ...zon.Lambda.AspNetCoreServer.Hosting.csproj | 6 ++ .../Internal/LambdaRuntimeSupportServer.cs | 3 - ...tartExecuteRequestsBeforeSnapshotHelper.cs | 42 +++++++++++++- .../Amazon.Lambda.RuntimeSupport.csproj | 13 +++++ .../Helpers/SnapstartHelperLambdaRequests.cs | 56 ------------------- 5 files changed, 60 insertions(+), 60 deletions(-) delete mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.cs diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj index d0c71e18b..60c81ea6b 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj @@ -21,6 +21,12 @@ + + + ..\..\..\buildtools\public.snk + true + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index 597c1877b..a62c41fb8 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -1,10 +1,7 @@ using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Text.Json; using Amazon.Lambda.AspNetCoreServer.Internal; using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.RuntimeSupport.Helpers; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.Extensions.DependencyInjection; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs index 46395b8d6..caf977931 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Text; using System.Text.Json; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.RuntimeSupport; @@ -61,7 +62,6 @@ public void RegisterInitializerRequests(HandlerWrapper handlerWrapper) { var json = JsonSerializer.Serialize(req); - // TODO - inline await SnapstartHelperLambdaRequests.ExecuteSnapstartInitRequests(json, times: 5, handlerWrapper); } }); @@ -88,6 +88,46 @@ internal async Task Execute(HttpClient client) } } + private static class SnapstartHelperLambdaRequests + { + private static InternalLogger _logger = InternalLogger.GetDefaultLogger(); + + private static readonly RuntimeApiHeaders _fakeRuntimeApiHeaders = new(new Dictionary> + { + { RuntimeApiHeaders.HeaderAwsRequestId, new List() }, + { RuntimeApiHeaders.HeaderTraceId, new List() }, + { RuntimeApiHeaders.HeaderClientContext, new List() }, + { RuntimeApiHeaders.HeaderCognitoIdentity, new List() }, + { RuntimeApiHeaders.HeaderDeadlineMs, new List() }, + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List() }, + }); + + public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int times, HandlerWrapper handlerWrapper) + { + var dummyRequest = new InvocationRequest + { + InputStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonRequest)), + LambdaContext = new LambdaContext( + _fakeRuntimeApiHeaders, + new LambdaEnvironment(), + new SimpleLoggerWriter()) + }; + + for (var i = 0; i < times; i++) + { + try + { + _ = await handlerWrapper.Handler.Invoke(dummyRequest); + } + catch (Exception e) + { + Console.WriteLine("StartAsync: " + e.Message + e.StackTrace); + _logger.LogError(e, "StartAsync: Custom Warmup Failure: " + e.Message + e.StackTrace); + } + } + } + } + private class LambdaSnapstartInitializerHttpMessageHandler : HttpMessageHandler { private LambdaEventSource _lambdaEventSource; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj index 47ce7f796..f9070e88c 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj @@ -16,6 +16,19 @@ 9.0 + + + ..\..\..\buildtools\public.snk + true + + + + + <_Parameter1>Amazon.Lambda.AspNetCoreServer.Hosting, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4" + + + Exe $(DefineConstants);ExecutableOutputType diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.cs deleted file mode 100644 index 05a0c3b62..000000000 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperLambdaRequests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; - -namespace Amazon.Lambda.RuntimeSupport.Helpers -{ - /// - /// should not be public - /// this is public to allow LambdaRuntimeSupportServer access, - /// otherwise it would need access to and - /// - /// - /// TODO - inline to LambdaSnapstartExecuteRequestsBeforeSnapshotHelper - /// - public static class SnapstartHelperLambdaRequests - { - private static InternalLogger _logger = InternalLogger.GetDefaultLogger(); - - private static readonly RuntimeApiHeaders _fakeRuntimeApiHeaders = new(new Dictionary> - { - { RuntimeApiHeaders.HeaderAwsRequestId, new List() }, - { RuntimeApiHeaders.HeaderTraceId, new List() }, - { RuntimeApiHeaders.HeaderClientContext, new List() }, - { RuntimeApiHeaders.HeaderCognitoIdentity, new List() }, - { RuntimeApiHeaders.HeaderDeadlineMs, new List() }, - { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List() }, - }); - - public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int times, HandlerWrapper handlerWrapper) - { - var dummyRequest = new InvocationRequest - { - InputStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonRequest)), - LambdaContext = new LambdaContext( - _fakeRuntimeApiHeaders, - new LambdaEnvironment(), - new SimpleLoggerWriter()) - }; - - for (var i = 0; i < times; i++) - { - try - { - _ = await handlerWrapper.Handler.Invoke(dummyRequest); - } - catch (Exception e) - { - Console.WriteLine("StartAsync: " + e.Message + e.StackTrace); - _logger.LogError(e, "StartAsync: Custom Warmup Failure: " + e.Message + e.StackTrace); - } - } - } - } -} From 501847c63e117d82fe45136c278ddf4d9e2cf0be Mon Sep 17 00:00:00 2001 From: philip pittle Date: Wed, 12 Mar 2025 11:44:03 -0700 Subject: [PATCH 03/17] add auto version change --- .../changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json diff --git a/.autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json b/.autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json new file mode 100644 index 000000000..7a40133d6 --- /dev/null +++ b/.autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.AspNetCoreServer.Hosting", + "Type": "Patch", + "ChangelogMessages": [ + "Add AddAWSLambdaBeforeSnapshotRequest to support warming up the asp.net/lambda pipelines automatically during BeforeSnapshot callback." + ] + } + ] +} \ No newline at end of file From 2c980b6456256f41b472cc71c1035004526b1cf7 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Wed, 12 Mar 2025 22:47:35 -0700 Subject: [PATCH 04/17] add support for additional LambdaEventSource --- ...tartExecuteRequestsBeforeSnapshotHelper.cs | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs index caf977931..0df68e5aa 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -6,8 +6,10 @@ using System.Text; using System.Text.Json; using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.RuntimeSupport; using Amazon.Lambda.RuntimeSupport.Helpers; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; @@ -141,20 +143,66 @@ public LambdaSnapstartInitializerHttpMessageHandler(LambdaEventSource lambdaEven _lambdaEventSource = lambdaEventSource; } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - // Copy request to correct request, ie APIGatewayProxyRequest + if (null == request?.RequestUri) + return new HttpResponseMessage(HttpStatusCode.OK); - // TODO - IMPLEMENT - var translatedRequest = new APIGatewayProxyRequest + var duckRequest = new { - Path = request.RequestUri.MakeRelativeUri(BaseUri).ToString(), - HttpMethod = request.Method.ToString() + Body = await ReadContent(request), + Headers = request.Headers + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.FirstOrDefault(), + StringComparer.OrdinalIgnoreCase), + HttpMethod = request.Method.ToString(), + Path = request.RequestUri?.MakeRelativeUri(BaseUri).ToString() ?? string.Empty, + RawQuery = request.RequestUri?.Query, + Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) + }; + + object translatedRequest = _lambdaEventSource switch + { + LambdaEventSource.ApplicationLoadBalancer => new ApplicationLoadBalancerRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }, + LambdaEventSource.HttpApi => new APIGatewayProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }, + LambdaEventSource.RestApi => new APIGatewayHttpApiV2ProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + RawPath = duckRequest.Path, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), + RawQueryString = duckRequest.RawQuery + }, + _ => throw new NotImplementedException( + $"Unknown {nameof(LambdaEventSource)}: {Enum.GetName(_lambdaEventSource)}") }; CapturedHttpRequests.Add(translatedRequest); - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + return new HttpResponseMessage(HttpStatusCode.OK); + } + + private async Task ReadContent(HttpRequestMessage r) + { + if (r.Content == null) + return string.Empty; + + return await r.Content.ReadAsStringAsync(); } } } From 12795d9e1621aec8e4db40296cb8491781ad83da Mon Sep 17 00:00:00 2001 From: philip pittle Date: Thu, 13 Mar 2025 13:18:30 -0700 Subject: [PATCH 05/17] update documentation --- .../ServiceCollectionExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index daee9866e..b862dab6d 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -107,6 +107,8 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// // Example Minimal Api /// var builder = WebApplication.CreateSlimBuilder(args); /// + /// builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi); + /// /// // Initialize asp.net pipeline before Snapshot /// builder.Services.AddAWSLambdaBeforeSnapshotRequest(async httpClient => /// { From 1ee369957bf2fc35cc6145cccaa8aed24842789b Mon Sep 17 00:00:00 2001 From: philip pittle Date: Thu, 13 Mar 2025 22:01:11 -0700 Subject: [PATCH 06/17] Fix bugs in LambdaSnapstartExecuteRequestsBeforeSnapshotHelper and add unit tests --- Libraries/Libraries.sln | 7 +++ ...tartExecuteRequestsBeforeSnapshotHelper.cs | 34 +++++++---- .../AddAWSLambdaBeforeSnapshotRequestTests.cs | 60 +++++++++++++++++++ ...mbda.AspNetCoreServer.Hosting.Tests.csproj | 28 +++++++++ .../EnvironmentVariableHelper.cs | 21 +++++++ 5 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableHelper.cs diff --git a/Libraries/Libraries.sln b/Libraries/Libraries.sln index e256e4e8f..bd3278a8f 100644 --- a/Libraries/Libraries.sln +++ b/Libraries/Libraries.sln @@ -139,6 +139,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DynamoDBEvent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests", "test\Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests\Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests.csproj", "{074DB940-82BA-47D4-B888-C213D4220A82}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.AspNetCoreServer.Hosting.Tests", "test\Amazon.Lambda.AspNetCoreServer.Hosting.Tests\Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj", "{D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -381,6 +383,10 @@ Global {074DB940-82BA-47D4-B888-C213D4220A82}.Debug|Any CPU.Build.0 = Debug|Any CPU {074DB940-82BA-47D4-B888-C213D4220A82}.Release|Any CPU.ActiveCfg = Release|Any CPU {074DB940-82BA-47D4-B888-C213D4220A82}.Release|Any CPU.Build.0 = Release|Any CPU + {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -449,6 +455,7 @@ Global {A699E183-D0D4-4F26-A0A7-88DA5607F455} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {3400F4E9-BA12-4D3D-9BA1-2798AA8D0AFC} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} {074DB940-82BA-47D4-B888-C213D4220A82} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} + {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs index 0df68e5aa..f59a51af4 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -132,7 +132,7 @@ public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int ti private class LambdaSnapstartInitializerHttpMessageHandler : HttpMessageHandler { - private LambdaEventSource _lambdaEventSource; + private readonly LambdaEventSource _lambdaEventSource; public static Uri BaseUri { get; } = new Uri("http://localhost"); @@ -145,7 +145,7 @@ public LambdaSnapstartInitializerHttpMessageHandler(LambdaEventSource lambdaEven protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - if (null == request?.RequestUri) + if (null == request.RequestUri) return new HttpResponseMessage(HttpStatusCode.OK); var duckRequest = new @@ -157,7 +157,7 @@ protected override async Task SendAsync(HttpRequestMessage kvp => kvp.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase), HttpMethod = request.Method.ToString(), - Path = request.RequestUri?.MakeRelativeUri(BaseUri).ToString() ?? string.Empty, + Path = "/" + BaseUri.MakeRelativeUri(request.RequestUri), RawQuery = request.RequestUri?.Query, Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) }; @@ -172,21 +172,33 @@ protected override async Task SendAsync(HttpRequestMessage HttpMethod = duckRequest.HttpMethod, QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) }, - LambdaEventSource.HttpApi => new APIGatewayProxyRequest + LambdaEventSource.HttpApi => new APIGatewayHttpApiV2ProxyRequest { Body = duckRequest.Body, Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + RawPath = duckRequest.Path, + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = duckRequest.HttpMethod, + Path = duckRequest.Path + } + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), + RawQueryString = duckRequest.RawQuery }, - LambdaEventSource.RestApi => new APIGatewayHttpApiV2ProxyRequest + LambdaEventSource.RestApi => new APIGatewayProxyRequest { Body = duckRequest.Body, Headers = duckRequest.Headers, - RawPath = duckRequest.Path, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), - RawQueryString = duckRequest.RawQuery + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + HttpMethod = duckRequest.HttpMethod + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) }, _ => throw new NotImplementedException( $"Unknown {nameof(LambdaEventSource)}: {Enum.GetName(_lambdaEventSource)}") diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs new file mode 100644 index 000000000..335ba529e --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -0,0 +1,60 @@ + +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.AspNetCoreServer.Test; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Tests for +/// +public class AddAWSLambdaBeforeSnapshotRequestTests +{ + #if NET8_0_OR_GREATER + [Theory] + [InlineData(LambdaEventSource.HttpApi)] + [InlineData(LambdaEventSource.RestApi)] + [InlineData(LambdaEventSource.ApplicationLoadBalancer)] + public async Task VerifyCallbackIsInvoked(LambdaEventSource hostingType) + { + using var e1 = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", nameof(VerifyCallbackIsInvoked)); + using var e2 = new EnvironmentVariableHelper("AWS_LAMBDA_INITIALIZATION_TYPE", "snap-start"); + + var callbackDidTheCallback = false; + + var builder = WebApplication.CreateSlimBuilder(new string[0]); + + builder.Services.AddAWSLambdaHosting(hostingType); + // Initialize asp.net pipeline before Snapshot + builder.Services.AddAWSLambdaBeforeSnapshotRequest(async httpClient => + { + await httpClient.GetAsync($"/test"); + }); + + var app = builder.Build(); + + app.MapGet($"/test", + () => + { + callbackDidTheCallback = true; + return "Success"; + }); + + var serverTask = app.RunAsync(); + + // let the server run for a max of 500 ms + await Task.WhenAny( + serverTask, + Task.Delay(TimeSpan.FromMilliseconds(500))); + + // shut down server + await app.StopAsync(); + + Assert.True(callbackDidTheCallback); + } + #endif +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj new file mode 100644 index 000000000..6264cdf89 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + true + false + false + false + 1701;1702;1705;CS0618 + + + + + + + + + + + + + + + + + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableHelper.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableHelper.cs new file mode 100644 index 000000000..1a876177d --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableHelper.cs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; + +namespace Amazon.Lambda.AspNetCoreServer.Test; + +public class EnvironmentVariableHelper : IDisposable +{ + private string _name; + private string? _oldValue; + public EnvironmentVariableHelper(string name, string value) + { + _name = name; + _oldValue = Environment.GetEnvironmentVariable(name); + + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() => Environment.SetEnvironmentVariable(_name, _oldValue); +} From c8f5db615969885c58177af1b2c6a442a353daa6 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Tue, 25 Mar 2025 16:38:03 -0700 Subject: [PATCH 07/17] removed duplicate reference to snk --- .../Amazon.Lambda.AspNetCoreServer.Hosting.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj index 60c81ea6b..d0c71e18b 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj @@ -21,12 +21,6 @@ - - - ..\..\..\buildtools\public.snk - true - From 6b2c6342051c714cab5a41c2259476cc962ca6fc Mon Sep 17 00:00:00 2001 From: philip pittle Date: Tue, 25 Mar 2025 16:34:31 -0700 Subject: [PATCH 08/17] update AddAWSLambdaBeforeSnapshotRequest() to support AOT by optimizing json serialization, adjust #if blocks and fix simple warnings --- .../Internal/LambdaRuntimeSupportServer.cs | 8 +- ...tartExecuteRequestsBeforeSnapshotHelper.cs | 125 +++++++++++------- .../ServiceCollectionExtensions.cs | 11 ++ 3 files changed, 92 insertions(+), 52 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index a62c41fb8..ab075da2e 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -16,7 +16,10 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal public abstract class LambdaRuntimeSupportServer : LambdaServer { private readonly IServiceProvider _serviceProvider; + + #if NET8_0_OR_GREATER private readonly LambdaSnapstartExecuteRequestsBeforeSnapshotHelper _snapstartInitHelper; + #endif internal ILambdaSerializer Serializer; @@ -27,7 +30,11 @@ public abstract class LambdaRuntimeSupportServer : LambdaServer public LambdaRuntimeSupportServer(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; + + #if NET8_0_OR_GREATER _snapstartInitHelper = _serviceProvider.GetRequiredService(); + #endif + Serializer = serviceProvider.GetRequiredService(); } @@ -38,7 +45,6 @@ public LambdaRuntimeSupportServer(IServiceProvider serviceProvider) /// /// /// - [RequiresUnreferencedCode("_snapstartInitHelper Serializes objects to Json")] public override Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) { base.StartAsync(application, cancellationToken); diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs index f59a51af4..111914642 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -5,6 +5,7 @@ using System.Net; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.RuntimeSupport; @@ -14,6 +15,8 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +#if NET8_0_OR_GREATER + /// /// Contains the plumbing to register a user provided inside /// . @@ -22,7 +25,7 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; /// performance gains offered by SnapStart. /// /// It works by construction a specialized that will intercept requests -/// and saved them inside . +/// and saved them inside . /// /// Intercepted requests are then be processed later by /// which will route them correctly through a simulated asp.net/lambda pipeline. @@ -37,11 +40,8 @@ public LambdaSnapstartExecuteRequestsBeforeSnapshotHelper(LambdaEventSource lamb } /// - [RequiresUnreferencedCode("Serializes object to json")] public void RegisterInitializerRequests(HandlerWrapper handlerWrapper) { - #if NET8_0_OR_GREATER - Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => { // Construct specialized HttpClient that will intercept requests and saved them inside @@ -60,39 +60,36 @@ public void RegisterInitializerRequests(HandlerWrapper handlerWrapper) // Request are now in CapturedHttpRequests. Serialize each one into a json object // and execute the request through the lambda pipeline (ie handlerWrapper). - foreach (var req in LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequests) + foreach (var json in LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequestsJson) { - var json = JsonSerializer.Serialize(req); - await SnapstartHelperLambdaRequests.ExecuteSnapstartInitRequests(json, times: 5, handlerWrapper); } }); - - #endif } + /// internal static BeforeSnapstartRequestRegistrar Registrar = new(); internal class BeforeSnapstartRequestRegistrar { - private List> beforeSnapstartFuncs = new(); + private readonly List> _beforeSnapstartFuncs = new(); public void Register(Func beforeSnapstartRequest) { - beforeSnapstartFuncs.Add(beforeSnapstartRequest); + _beforeSnapstartFuncs.Add(beforeSnapstartRequest); } internal async Task Execute(HttpClient client) { - foreach (var f in beforeSnapstartFuncs) + foreach (var f in _beforeSnapstartFuncs) await f(client); } } private static class SnapstartHelperLambdaRequests { - private static InternalLogger _logger = InternalLogger.GetDefaultLogger(); + private static readonly InternalLogger _logger = InternalLogger.GetDefaultLogger(); private static readonly RuntimeApiHeaders _fakeRuntimeApiHeaders = new(new Dictionary> { @@ -136,7 +133,7 @@ private class LambdaSnapstartInitializerHttpMessageHandler : HttpMessageHandler public static Uri BaseUri { get; } = new Uri("http://localhost"); - public static List CapturedHttpRequests { get; } = new(); + public static List CapturedHttpRequestsJson { get; } = new(); public LambdaSnapstartInitializerHttpMessageHandler(LambdaEventSource lambdaEventSource) { @@ -162,49 +159,60 @@ protected override async Task SendAsync(HttpRequestMessage Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) }; - object translatedRequest = _lambdaEventSource switch + string translatedRequestJson = _lambdaEventSource switch { - LambdaEventSource.ApplicationLoadBalancer => new ApplicationLoadBalancerRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }, - LambdaEventSource.HttpApi => new APIGatewayHttpApiV2ProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - RawPath = duckRequest.Path, - RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext - { - Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + LambdaEventSource.ApplicationLoadBalancer => + JsonSerializer.Serialize( + new ApplicationLoadBalancerRequest { - Method = duckRequest.HttpMethod, - Path = duckRequest.Path - } - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), - RawQueryString = duckRequest.RawQuery - }, - LambdaEventSource.RestApi => new APIGatewayProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - RequestContext = new APIGatewayProxyRequest.ProxyRequestContext - { - HttpMethod = duckRequest.HttpMethod - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }, + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }, + LambdaRequestTypeClasses.Default.ApplicationLoadBalancerRequest), + LambdaEventSource.HttpApi => + JsonSerializer.Serialize( + new APIGatewayHttpApiV2ProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + RawPath = duckRequest.Path, + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = duckRequest.HttpMethod, + Path = duckRequest.Path + } + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), + RawQueryString = duckRequest.RawQuery + }, + LambdaRequestTypeClasses.Default.APIGatewayHttpApiV2ProxyRequest), + LambdaEventSource.RestApi => + JsonSerializer.Serialize( + new APIGatewayProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + HttpMethod = duckRequest.HttpMethod + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }, + LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), _ => throw new NotImplementedException( $"Unknown {nameof(LambdaEventSource)}: {Enum.GetName(_lambdaEventSource)}") }; - CapturedHttpRequests.Add(translatedRequest); + // NOTE: Any object added to CapturedHttpRequests must have it's type added + // to the + CapturedHttpRequestsJson.Add(translatedRequestJson); return new HttpResponseMessage(HttpStatusCode.OK); } @@ -218,3 +226,18 @@ private async Task ReadContent(HttpRequestMessage r) } } } + + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(ApplicationLoadBalancerRequest))] +[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] +[JsonSerializable(typeof(APIGatewayProxyRequest))] +[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity))] +[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert))] +[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] +[JsonSerializable(typeof(APIGatewayProxyRequest.RequestIdentity))] + +internal partial class LambdaRequestTypeClasses : JsonSerializerContext +{ +} +#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index b862dab6d..098688a35 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -88,6 +88,7 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser return services; } +#if NET8_0_OR_GREATER /// /// Adds a function meant to initialize the asp.net and lambda pipelines during /// improving the performance gains offered by SnapStart. @@ -123,9 +124,17 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// ]]> /// /// + #else + /// + /// Snapstart requires your application to target .NET 8 or above. + /// + #endif public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, Func beforeSnapStartRequest) { + + #if NET8_0_OR_GREATER LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.Registrar.Register(beforeSnapStartRequest); + #endif return services; } @@ -153,8 +162,10 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc Utilities.EnsureLambdaServerRegistered(services, serverType); + #if NET8_0_OR_GREATER // register a LambdaSnapStartInitializerHttpMessageHandler services.AddSingleton(new LambdaSnapstartExecuteRequestsBeforeSnapshotHelper(eventSource)); + #endif return true; } From 8108dd63767905fc2f42ce58a2b8357bff03afd8 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Wed, 2 Apr 2025 10:32:08 -0700 Subject: [PATCH 09/17] remove requirement that Amazon.Lambda.RuntimeSupport make InternalsVisibileTo Amazon.Lambda.AspNetCorServer.Hosting --- ...tartExecuteRequestsBeforeSnapshotHelper.cs | 48 +++++++++++-------- .../Amazon.Lambda.RuntimeSupport.csproj | 6 --- .../Client/InvocationRequest.cs | 7 +++ 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs index 111914642..fa8a99265 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -8,6 +8,7 @@ using System.Text.Json.Serialization; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; using Amazon.Lambda.RuntimeSupport.Helpers; using Microsoft.AspNetCore.WebUtilities; @@ -87,30 +88,36 @@ internal async Task Execute(HttpClient client) } } - private static class SnapstartHelperLambdaRequests + private class HelperLambdaContext : ILambdaContext, ICognitoIdentity, IClientContext { - private static readonly InternalLogger _logger = InternalLogger.GetDefaultLogger(); - - private static readonly RuntimeApiHeaders _fakeRuntimeApiHeaders = new(new Dictionary> - { - { RuntimeApiHeaders.HeaderAwsRequestId, new List() }, - { RuntimeApiHeaders.HeaderTraceId, new List() }, - { RuntimeApiHeaders.HeaderClientContext, new List() }, - { RuntimeApiHeaders.HeaderCognitoIdentity, new List() }, - { RuntimeApiHeaders.HeaderDeadlineMs, new List() }, - { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List() }, - }); + private LambdaEnvironment _lambdaEnvironment = new (); + + public string TraceId => string.Empty; + public string AwsRequestId => string.Empty; + public IClientContext ClientContext => this; + public string FunctionName => _lambdaEnvironment.FunctionName; + public string FunctionVersion => _lambdaEnvironment.FunctionVersion; + public ICognitoIdentity Identity => this; + public string InvokedFunctionArn => string.Empty; + public ILambdaLogger Logger => null; + public string LogGroupName => _lambdaEnvironment.LogGroupName; + public string LogStreamName => _lambdaEnvironment.LogStreamName; + public int MemoryLimitInMB => 128; + public TimeSpan RemainingTime => TimeSpan.FromMilliseconds(100); + public string IdentityId { get; } + public string IdentityPoolId { get; } + public IDictionary Environment { get; } = new Dictionary(); + public IClientApplication Client { get; } + public IDictionary Custom { get; } = new Dictionary(); + } + private static class SnapstartHelperLambdaRequests + { public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int times, HandlerWrapper handlerWrapper) { - var dummyRequest = new InvocationRequest - { - InputStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonRequest)), - LambdaContext = new LambdaContext( - _fakeRuntimeApiHeaders, - new LambdaEnvironment(), - new SimpleLoggerWriter()) - }; + var dummyRequest = new InvocationRequest( + new MemoryStream(Encoding.UTF8.GetBytes(jsonRequest)), + new HelperLambdaContext()); for (var i = 0; i < times; i++) { @@ -121,7 +128,6 @@ public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int ti catch (Exception e) { Console.WriteLine("StartAsync: " + e.Message + e.StackTrace); - _logger.LogError(e, "StartAsync: Custom Warmup Failure: " + e.Message + e.StackTrace); } } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj index f9070e88c..1f72bebfc 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj @@ -23,12 +23,6 @@ true - - - <_Parameter1>Amazon.Lambda.AspNetCoreServer.Hosting, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4" - - - Exe $(DefineConstants);ExecutableOutputType diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs index 93d5bf151..7412fa1ab 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs @@ -35,6 +35,13 @@ public class InvocationRequest : IDisposable internal InvocationRequest() { } + /// > + public InvocationRequest(Stream inputStream, ILambdaContext lambdaContext) + { + InputStream = inputStream; + LambdaContext = lambdaContext; + } + public void Dispose() { InputStream?.Dispose(); From ac2d49bb1250cbce06c30d8ff119975e14d51ae7 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Wed, 2 Apr 2025 11:41:51 -0700 Subject: [PATCH 10/17] migrate extension method to use HttpRequestMethods instead of collecting calls to HttpClient --- ...tartExecuteRequestsBeforeSnapshotHelper.cs | 80 +++++++------------ .../ServiceCollectionExtensions.cs | 34 +++++--- .../AddAWSLambdaBeforeSnapshotRequestTests.cs | 7 +- 3 files changed, 57 insertions(+), 64 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs index fa8a99265..9992d390d 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -45,24 +45,10 @@ public void RegisterInitializerRequests(HandlerWrapper handlerWrapper) { Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => { - // Construct specialized HttpClient that will intercept requests and saved them inside - // LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequests. - // - // They will be processed later by SnapstartHelperLambdaRequests.ExecuteSnapstartInitRequests which will - // route them correctly through a simulated lambda pipeline. - var messageHandlerThatCollectsRequests = new LambdaSnapstartInitializerHttpMessageHandler(_lambdaEventSource); - - var httpClientThatCollectsRequests = new HttpClient(messageHandlerThatCollectsRequests); - httpClientThatCollectsRequests.BaseAddress = LambdaSnapstartInitializerHttpMessageHandler.BaseUri; - - // "Invoke" each registered request function. Requests will be captured inside. - // LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequests. - await Registrar.Execute(httpClientThatCollectsRequests); - - // Request are now in CapturedHttpRequests. Serialize each one into a json object - // and execute the request through the lambda pipeline (ie handlerWrapper). - foreach (var json in LambdaSnapstartInitializerHttpMessageHandler.CapturedHttpRequestsJson) + foreach (var req in Registrar.GetAllRequests()) { + var json = await SnapstartHelperLambdaRequests.SerializeToJson(req, _lambdaEventSource); + await SnapstartHelperLambdaRequests.ExecuteSnapstartInitRequests(json, times: 5, handlerWrapper); } }); @@ -74,17 +60,19 @@ public void RegisterInitializerRequests(HandlerWrapper handlerWrapper) internal class BeforeSnapstartRequestRegistrar { - private readonly List> _beforeSnapstartFuncs = new(); + private readonly List>> _beforeSnapstartRequests = new(); - public void Register(Func beforeSnapstartRequest) + + public void Register(Func> beforeSnapstartRequests) { - _beforeSnapstartFuncs.Add(beforeSnapstartRequest); + _beforeSnapstartRequests.Add(beforeSnapstartRequests); } - internal async Task Execute(HttpClient client) + internal IEnumerable GetAllRequests() { - foreach (var f in _beforeSnapstartFuncs) - await f(client); + foreach (var batch in _beforeSnapstartRequests) + foreach (var r in batch()) + yield return r; } } @@ -113,6 +101,8 @@ private class HelperLambdaContext : ILambdaContext, ICognitoIdentity, IClientCon private static class SnapstartHelperLambdaRequests { + private static readonly Uri _baseUri = new Uri("http://localhost"); + public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int times, HandlerWrapper handlerWrapper) { var dummyRequest = new InvocationRequest( @@ -131,25 +121,21 @@ public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int ti } } } - } - - private class LambdaSnapstartInitializerHttpMessageHandler : HttpMessageHandler - { - private readonly LambdaEventSource _lambdaEventSource; - - public static Uri BaseUri { get; } = new Uri("http://localhost"); - - public static List CapturedHttpRequestsJson { get; } = new(); - - public LambdaSnapstartInitializerHttpMessageHandler(LambdaEventSource lambdaEventSource) - { - _lambdaEventSource = lambdaEventSource; - } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public static async Task SerializeToJson(HttpRequestMessage request, LambdaEventSource lambdaType) { if (null == request.RequestUri) - return new HttpResponseMessage(HttpStatusCode.OK); + { + throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be set.", nameof(request)); + } + + if (request.RequestUri.IsAbsoluteUri) + { + throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be relative.", nameof(request)); + } + + // make request absolut (relative to localhost) otherwise parsing the query will not work + request.RequestUri = new Uri(_baseUri, request.RequestUri); var duckRequest = new { @@ -160,12 +146,12 @@ protected override async Task SendAsync(HttpRequestMessage kvp => kvp.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase), HttpMethod = request.Method.ToString(), - Path = "/" + BaseUri.MakeRelativeUri(request.RequestUri), + Path = "/" + _baseUri.MakeRelativeUri(request.RequestUri), RawQuery = request.RequestUri?.Query, Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) }; - - string translatedRequestJson = _lambdaEventSource switch + + string translatedRequestJson = lambdaType switch { LambdaEventSource.ApplicationLoadBalancer => JsonSerializer.Serialize( @@ -213,17 +199,13 @@ protected override async Task SendAsync(HttpRequestMessage }, LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), _ => throw new NotImplementedException( - $"Unknown {nameof(LambdaEventSource)}: {Enum.GetName(_lambdaEventSource)}") + $"Unknown {nameof(LambdaEventSource)}: {Enum.GetName(lambdaType)}") }; - // NOTE: Any object added to CapturedHttpRequests must have it's type added - // to the - CapturedHttpRequestsJson.Add(translatedRequestJson); - - return new HttpResponseMessage(HttpStatusCode.OK); + return translatedRequestJson; } - private async Task ReadContent(HttpRequestMessage r) + private static async Task ReadContent(HttpRequestMessage r) { if (r.Content == null) return string.Empty; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 098688a35..e0091ff29 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -88,18 +88,22 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser return services; } -#if NET8_0_OR_GREATER + #if NET8_0_OR_GREATER /// /// Adds a function meant to initialize the asp.net and lambda pipelines during /// improving the performance gains offered by SnapStart. /// - /// Use the passed to invoke one or more Routes in your lambda function. + /// Pass a function with one or more s that will be used to invoke + /// Routes in your lambda function. The returned must have a relative + /// . + /// . /// Be aware that this will invoke your applications function handler code - /// multiple times. It uses a mock which may not be fully populated. + /// multiple times. Additionally, tt uses a mock + /// which may not be fully populated. /// /// This method automatically registers with . /// - /// If SnapStart is not enabled, then this method is ignored and is never invoked. + /// If SnapStart is not enabled, then this method is ignored and is never invoked. /// /// Example: /// @@ -111,14 +115,13 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi); /// /// // Initialize asp.net pipeline before Snapshot - /// builder.Services.AddAWSLambdaBeforeSnapshotRequest(async httpClient => - /// { - /// await httpClient.GetAsync($"/test"); + /// builder.Services.AddAWSLambdaBeforeSnapshotRequest(() => [ + /// new HttpRequestMessage(HttpMethod.Get, "/test") /// }); /// /// var app = builder.Build(); /// - /// app.MapGet($"/test", () => "Success"); + /// app.MapGet("/test", () => "Success"); /// /// app.Run(); /// ]]> @@ -129,16 +132,25 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// Snapstart requires your application to target .NET 8 or above. /// #endif - public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, Func beforeSnapStartRequest) + public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, Func> beforeSnapStartRequests) { - #if NET8_0_OR_GREATER - LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.Registrar.Register(beforeSnapStartRequest); + LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.Registrar.Register(beforeSnapStartRequests); #endif return services; } + /// + public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, Func beforeSnapStartRequest) + { + return AddAWSLambdaBeforeSnapshotRequest(services, + () => new List + { + beforeSnapStartRequest() + }); + } + private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSource eventSource, Action? configure, out HostingOptions? hostingOptions) { hostingOptions = null; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs index 335ba529e..1f761bfcd 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -30,10 +30,9 @@ public async Task VerifyCallbackIsInvoked(LambdaEventSource hostingType) builder.Services.AddAWSLambdaHosting(hostingType); // Initialize asp.net pipeline before Snapshot - builder.Services.AddAWSLambdaBeforeSnapshotRequest(async httpClient => - { - await httpClient.GetAsync($"/test"); - }); + builder.Services.AddAWSLambdaBeforeSnapshotRequest(() => + new HttpRequestMessage(HttpMethod.Get, "/test") + ); var app = builder.Build(); From d30e76f7edea5059dae6a484c15a9e8c19dd3334 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Wed, 2 Apr 2025 11:42:32 -0700 Subject: [PATCH 11/17] Add Snapstart helper to AbstractAspNetCoreFunction --- ...zon.Lambda.AspNetCoreServer.Hosting.csproj | 4 +- ...tartExecuteRequestsBeforeSnapshotHelper.cs | 101 +---------- .../ServiceCollectionExtensions.cs | 2 +- .../AbstractAspNetCoreFunction.cs | 73 ++++++++ .../Amazon.Lambda.AspNetCoreServer.csproj | 1 + .../Internal/HttpRequestMessageSerializer.cs | 163 ++++++++++++++++++ .../Internal/SnapStartEmptyLambdaContext.cs | 32 ++++ .../TestApiGatewayHttpApiV2Calls.cs | 16 +- .../Controllers/SnapStartController.cs | 24 +++ .../TestWebApp/HttpApiV2LambdaFunction.cs | 17 +- 10 files changed, 333 insertions(+), 100 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs create mode 100644 Libraries/test/TestWebApp/Controllers/SnapStartController.cs diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj index d0c71e18b..dc756ec7d 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj @@ -27,6 +27,8 @@ - + + + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs index 9992d390d..0a0b75f06 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs @@ -8,6 +8,7 @@ using System.Text.Json.Serialization; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Amazon.Lambda.AspNetCoreServer.Internal; using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; using Amazon.Lambda.RuntimeSupport.Helpers; @@ -101,8 +102,6 @@ private class HelperLambdaContext : ILambdaContext, ICognitoIdentity, IClientCon private static class SnapstartHelperLambdaRequests { - private static readonly Uri _baseUri = new Uri("http://localhost"); - public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int times, HandlerWrapper handlerWrapper) { var dummyRequest = new InvocationRequest( @@ -124,108 +123,20 @@ public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int ti public static async Task SerializeToJson(HttpRequestMessage request, LambdaEventSource lambdaType) { - if (null == request.RequestUri) - { - throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be set.", nameof(request)); - } - - if (request.RequestUri.IsAbsoluteUri) - { - throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be relative.", nameof(request)); - } - - // make request absolut (relative to localhost) otherwise parsing the query will not work - request.RequestUri = new Uri(_baseUri, request.RequestUri); - - var duckRequest = new - { - Body = await ReadContent(request), - Headers = request.Headers - .ToDictionary( - kvp => kvp.Key, - kvp => kvp.Value.FirstOrDefault(), - StringComparer.OrdinalIgnoreCase), - HttpMethod = request.Method.ToString(), - Path = "/" + _baseUri.MakeRelativeUri(request.RequestUri), - RawQuery = request.RequestUri?.Query, - Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) - }; - - string translatedRequestJson = lambdaType switch + var result = lambdaType switch { LambdaEventSource.ApplicationLoadBalancer => - JsonSerializer.Serialize( - new ApplicationLoadBalancerRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }, - LambdaRequestTypeClasses.Default.ApplicationLoadBalancerRequest), + await HttpRequestMessageSerializer.SerializeToJson(request), LambdaEventSource.HttpApi => - JsonSerializer.Serialize( - new APIGatewayHttpApiV2ProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - RawPath = duckRequest.Path, - RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext - { - Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription - { - Method = duckRequest.HttpMethod, - Path = duckRequest.Path - } - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), - RawQueryString = duckRequest.RawQuery - }, - LambdaRequestTypeClasses.Default.APIGatewayHttpApiV2ProxyRequest), + await HttpRequestMessageSerializer.SerializeToJson(request), LambdaEventSource.RestApi => - JsonSerializer.Serialize( - new APIGatewayProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - RequestContext = new APIGatewayProxyRequest.ProxyRequestContext - { - HttpMethod = duckRequest.HttpMethod - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }, - LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), + await HttpRequestMessageSerializer.SerializeToJson(request), _ => throw new NotImplementedException( $"Unknown {nameof(LambdaEventSource)}: {Enum.GetName(lambdaType)}") }; - return translatedRequestJson; - } - - private static async Task ReadContent(HttpRequestMessage r) - { - if (r.Content == null) - return string.Empty; - - return await r.Content.ReadAsStringAsync(); + return result; } } } - - -[JsonSourceGenerationOptions(WriteIndented = true)] -[JsonSerializable(typeof(ApplicationLoadBalancerRequest))] -[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] -[JsonSerializable(typeof(APIGatewayProxyRequest))] -[JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity))] -[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert))] -[JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] -[JsonSerializable(typeof(APIGatewayProxyRequest.RequestIdentity))] - -internal partial class LambdaRequestTypeClasses : JsonSerializerContext -{ -} #endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index e0091ff29..000f970aa 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -98,7 +98,7 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// . /// . /// Be aware that this will invoke your applications function handler code - /// multiple times. Additionally, tt uses a mock + /// multiple times. Additionally, it uses a mock /// which may not be fully populated. /// /// This method automatically registers with . diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index 2a3ed020c..5cf02d5d4 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net.Http; using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -251,6 +253,46 @@ protected virtual IHostBuilder CreateHostBuilder() return builder; } + #if NET8_0_OR_GREATER + /// + /// Return one or more s that will be used to invoke + /// Routes in your lambda function in order to initialize the asp.net and lambda pipelines + /// during , + /// improving the performance gains offered by SnapStart. + /// + /// The returned s must have a relative + /// . + /// . + /// Be aware that this will invoke your applications function handler code + /// multiple times. Additionally, it uses a mock + /// which may not be fully populated. + /// + /// This method automatically registers with . + /// + /// If SnapStart is not enabled, then this method is never invoked. + /// + /// Example: + /// + /// + /// + /// { + /// protected override IEnumerable RegisterBeforeSnapshotRequest() => + /// [ + /// new HttpRequestMessage + /// { + /// RequestUri = new Uri("/api/ExampleSnapstartInit"), + /// Method = HttpMethod.Get + /// } + /// ]; + /// } + /// ]]> + /// + /// + protected virtual IEnumerable RegisterBeforeSnapshotRequest() => + Enumerable.Empty(); + #endif + private protected bool IsStarted { get @@ -284,6 +326,37 @@ protected void Start() "instead of ConfigureWebHostDefaults to make sure the property Lambda services are registered."); } _logger = ActivatorUtilities.CreateInstance>>(this._hostServices); + + #if NET8_0_OR_GREATER + + Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => + { + var beforeSnapstartRequests = RegisterBeforeSnapshotRequest(); + + foreach (var httpRequest in beforeSnapstartRequests) + { + var invokeTimes = 5; + + var json = await HttpRequestMessageSerializer.SerializeToJson(httpRequest); + + var request = HttpRequestMessageSerializer.Deserialize(json); + + InvokeFeatures features = new InvokeFeatures(); + MarshallRequest(features, request, new SnapStartEmptyLambdaContext()); + + var context = CreateContext(features); + + var lambdaContext = new SnapStartEmptyLambdaContext(); + + for (var i = 0; i < invokeTimes; i++) + { + await ProcessRequest(lambdaContext, context, features); + } + } + }); + + #endif + } /// diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj index 2cfdbfb4f..c94b36a84 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj @@ -31,6 +31,7 @@ + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs new file mode 100644 index 000000000..7f2f44fdf --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Microsoft.AspNetCore.WebUtilities; + +#if NET8_0_OR_GREATER +namespace Amazon.Lambda.AspNetCoreServer.Internal +{ + /// + /// Helper class for converting a to a known + /// lambda request type like: , + /// , or . + /// + /// Object is returned as a serialized string. + /// + /// This is intended for internal use to support Snapstart initialization. Not all properties + /// may be full set. + /// + public class HttpRequestMessageSerializer + { + private static readonly Uri _baseUri = new Uri("http://localhost"); + + public static async Task SerializeToJson(HttpRequestMessage request) + { + if (null == request.RequestUri) + { + throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be set.", nameof(request)); + } + + if (request.RequestUri.IsAbsoluteUri) + { + throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be relative.", nameof(request)); + } + + // make request absolut (relative to localhost) otherwise parsing the query will not work + request.RequestUri = new Uri(_baseUri, request.RequestUri); + + var duckRequest = new + { + Body = await ReadContent(request), + Headers = request.Headers + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.FirstOrDefault(), + StringComparer.OrdinalIgnoreCase), + HttpMethod = request.Method.ToString(), + Path = "/" + _baseUri.MakeRelativeUri(request.RequestUri), + RawQuery = request.RequestUri?.Query, + Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) + }; + + string translatedRequestJson = typeof(TRequest) switch + { + var t when t == typeof(ApplicationLoadBalancerRequest) => + JsonSerializer.Serialize( + new ApplicationLoadBalancerRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }, + LambdaRequestTypeClasses.Default.ApplicationLoadBalancerRequest), + var t when t == typeof(APIGatewayHttpApiV2ProxyRequest) => + JsonSerializer.Serialize( + new APIGatewayHttpApiV2ProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + RawPath = duckRequest.Path, + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = duckRequest.HttpMethod, + Path = duckRequest.Path + } + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), + RawQueryString = duckRequest.RawQuery + }, + LambdaRequestTypeClasses.Default.APIGatewayHttpApiV2ProxyRequest), + var t when t == typeof(APIGatewayProxyRequest) => + JsonSerializer.Serialize( + new APIGatewayProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + HttpMethod = duckRequest.HttpMethod + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }, + LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), + _ => throw new NotImplementedException( + $"Unknown request type: {typeof(TRequest).FullName}") + }; + + return translatedRequestJson; + } + + /// + /// Specialized Deserializer that uses the AOT Compatible + /// to deserialize common + /// Request types. + /// + public static TRequest Deserialize(string json) + { + return typeof(TRequest) switch + { + var t when t == typeof(ApplicationLoadBalancerRequest) => + JsonSerializer.Deserialize(json, LambdaRequestTypeClasses.Default.ApplicationLoadBalancerRequest), + var t when t == typeof(APIGatewayHttpApiV2ProxyRequest) => + JsonSerializer.Deserialize(json, LambdaRequestTypeClasses.Default.APIGatewayHttpApiV2ProxyRequest), + var t when t == typeof(APIGatewayProxyRequest) => + JsonSerializer.Deserialize(json, LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), + _ => throw new NotImplementedException( + $"Unknown request type: {typeof(TRequest).FullName}") + }; + } + + private static async Task ReadContent(HttpRequestMessage r) + { + if (r.Content == null) + return string.Empty; + + return await r.Content.ReadAsStringAsync(); + } + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ApplicationLoadBalancerRequest))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestAuthentication))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.HttpDescription))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.IAMDescription))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.CognitoIdentityDescription))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.JwtDescription))] + [JsonSerializable(typeof(APIGatewayProxyRequest))] + [JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity))] + [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert))] + [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] + [JsonSerializable(typeof(APIGatewayProxyRequest.RequestIdentity))] + internal partial class LambdaRequestTypeClasses : JsonSerializerContext + { + } + } +} +#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs new file mode 100644 index 000000000..fe98c0cda --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; + +namespace Amazon.Lambda.AspNetCoreServer.Internal; + +internal class SnapStartEmptyLambdaContext : ILambdaContext, ICognitoIdentity, IClientContext +{ + private LambdaEnvironment _lambdaEnvironment = new(); + + public string TraceId => string.Empty; + public string AwsRequestId => string.Empty; + public IClientContext ClientContext => this; + public string FunctionName => _lambdaEnvironment.FunctionName; + public string FunctionVersion => _lambdaEnvironment.FunctionVersion; + public ICognitoIdentity Identity => this; + public string InvokedFunctionArn => string.Empty; + public ILambdaLogger Logger => null; + public string LogGroupName => _lambdaEnvironment.LogGroupName; + public string LogStreamName => _lambdaEnvironment.LogStreamName; + public int MemoryLimitInMB => 128; + public TimeSpan RemainingTime => TimeSpan.FromMilliseconds(100); + public string IdentityId { get; } + public string IdentityPoolId { get; } + public IDictionary Environment { get; } = new Dictionary(); + public IClientApplication Client { get; } + public IDictionary Custom { get; } = new Dictionary(); +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs index 8b6fe973c..a00a19dd6 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs @@ -15,7 +15,7 @@ using Newtonsoft.Json.Linq; using TestWebApp; - +using TestWebApp.Controllers; using Xunit; @@ -282,6 +282,20 @@ public async Task TestTraceIdSetFromLambdaContext() } } + #if NET8_0_OR_GREATER + /// + /// Verifies that is invoked during startup. + /// + /// + [Fact] + public void TestSnapstartInititalizaton() + { + var lambdaFunction = new TestWebApp.HttpV2LambdaFunction(); + + Assert.True(SnapStartController.Invoked); + } + #endif + private async Task InvokeAPIGatewayRequest(string fileName, bool configureApiToReturnExceptionDetail = false) { return await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), GetRequestContent(fileName), configureApiToReturnExceptionDetail); diff --git a/Libraries/test/TestWebApp/Controllers/SnapStartController.cs b/Libraries/test/TestWebApp/Controllers/SnapStartController.cs new file mode 100644 index 000000000..58ffdc448 --- /dev/null +++ b/Libraries/test/TestWebApp/Controllers/SnapStartController.cs @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc; + +namespace TestWebApp.Controllers; + +[Route("api/[controller]")] +public class SnapStartController +{ + /// + /// Set when is invoked + /// + public static bool Invoked { get; set; } + + + [HttpGet] + public string Get() + { + Invoked = true; + + return "Invoked set to true"; + } +} diff --git a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs index 596eac4b7..5d1ab46d4 100644 --- a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs +++ b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs @@ -1,7 +1,20 @@ -using Amazon.Lambda.AspNetCoreServer; +using System.Collections.Generic; +using System.Net.Http; +using System; +using Amazon.Lambda.AspNetCoreServer; namespace TestWebApp { public class HttpV2LambdaFunction : APIGatewayHttpApiV2ProxyFunction { +#if NET8_0_OR_GREATER + protected override IEnumerable RegisterBeforeSnapshotRequest() => + [ + new HttpRequestMessage + { + RequestUri = new Uri("/api/Snapstart"), + Method = HttpMethod.Get + } + ]; +#endif } -} \ No newline at end of file +} From 1b9622cdaafd9a5dceaca8f45b75067d3b4e1c84 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Sun, 20 Apr 2025 13:37:16 -0700 Subject: [PATCH 12/17] rename RegisterBeforeSnapshotRequest to GetBeforeSnapshotRequests --- .../AbstractAspNetCoreFunction.cs | 4 ++-- .../TestApiGatewayHttpApiV2Calls.cs | 2 +- Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index 5cf02d5d4..590c2169c 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -289,7 +289,7 @@ protected virtual IHostBuilder CreateHostBuilder() /// ]]> /// /// - protected virtual IEnumerable RegisterBeforeSnapshotRequest() => + protected virtual IEnumerable GetBeforeSnapshotRequests() => Enumerable.Empty(); #endif @@ -331,7 +331,7 @@ protected void Start() Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => { - var beforeSnapstartRequests = RegisterBeforeSnapshotRequest(); + var beforeSnapstartRequests = GetBeforeSnapshotRequests(); foreach (var httpRequest in beforeSnapstartRequests) { diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs index a00a19dd6..b91255b5e 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs @@ -284,7 +284,7 @@ public async Task TestTraceIdSetFromLambdaContext() #if NET8_0_OR_GREATER /// - /// Verifies that is invoked during startup. + /// Verifies that is invoked during startup. /// /// [Fact] diff --git a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs index 5d1ab46d4..0561b2b84 100644 --- a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs +++ b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs @@ -7,7 +7,7 @@ namespace TestWebApp public class HttpV2LambdaFunction : APIGatewayHttpApiV2ProxyFunction { #if NET8_0_OR_GREATER - protected override IEnumerable RegisterBeforeSnapshotRequest() => + protected override IEnumerable GetBeforeSnapshotRequests() => [ new HttpRequestMessage { From 71540b3fdd3342b3fa46dc7f602b099ebdd3fa22 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Sun, 20 Apr 2025 13:39:12 -0700 Subject: [PATCH 13/17] optimize HttpRequestMessageSerializer and remove AspNetCoreServers's dependency on RuntimeSupport --- .../AbstractAspNetCoreFunction.cs | 8 +- .../Amazon.Lambda.AspNetCoreServer.csproj | 1 - .../Internal/HttpRequestMessageSerializer.cs | 155 ++++++++++-------- .../Internal/SnapStartEmptyLambdaContext.cs | 53 +++++- 4 files changed, 132 insertions(+), 85 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index 590c2169c..5fa1e50db 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -337,19 +337,17 @@ protected void Start() { var invokeTimes = 5; - var json = await HttpRequestMessageSerializer.SerializeToJson(httpRequest); - - var request = HttpRequestMessageSerializer.Deserialize(json); + var request = await HttpRequestMessageSerializer.ConvertToLambdaRequest(httpRequest); InvokeFeatures features = new InvokeFeatures(); MarshallRequest(features, request, new SnapStartEmptyLambdaContext()); var context = CreateContext(features); - var lambdaContext = new SnapStartEmptyLambdaContext(); - for (var i = 0; i < invokeTimes; i++) { + var lambdaContext = new SnapStartEmptyLambdaContext(); + await ProcessRequest(lambdaContext, context, features); } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj index c94b36a84..2cfdbfb4f 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj @@ -31,7 +31,6 @@ - diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs index 7f2f44fdf..f4904513d 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs @@ -1,3 +1,4 @@ +#if NET8_0_OR_GREATER using System; using System.Collections.Generic; using System.Linq; @@ -8,9 +9,10 @@ using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Microsoft.AspNetCore.Identity.Data; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; -#if NET8_0_OR_GREATER namespace Amazon.Lambda.AspNetCoreServer.Internal { /// @@ -26,8 +28,18 @@ namespace Amazon.Lambda.AspNetCoreServer.Internal public class HttpRequestMessageSerializer { private static readonly Uri _baseUri = new Uri("http://localhost"); + + internal class DuckRequest + { + public string Body { get; set; } + public Dictionary Headers { get; set; } = new(); + public string HttpMethod { get; set; } + public string Path { get; set; } + public string RawQuery { get; set; } + public Dictionary Query { get; set; } = new(); + } - public static async Task SerializeToJson(HttpRequestMessage request) + public static async Task ConvertToLambdaRequest(HttpRequestMessage request) { if (null == request.RequestUri) { @@ -42,7 +54,7 @@ public static async Task SerializeToJson(HttpRequestMessage re // make request absolut (relative to localhost) otherwise parsing the query will not work request.RequestUri = new Uri(_baseUri, request.RequestUri); - var duckRequest = new + var duckRequest = new DuckRequest { Body = await ReadContent(request), Headers = request.Headers @@ -56,52 +68,75 @@ public static async Task SerializeToJson(HttpRequestMessage re Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) }; + if (typeof(TRequest) == typeof(ApplicationLoadBalancerRequest)) + { + return (TRequest)(object) new ApplicationLoadBalancerRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }; + } + + if (typeof(TRequest) == typeof(APIGatewayHttpApiV2ProxyRequest)) + { + return (TRequest)(object)new APIGatewayHttpApiV2ProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + RawPath = duckRequest.Path, + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = duckRequest.HttpMethod, + Path = duckRequest.Path + } + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), + RawQueryString = duckRequest.RawQuery + }; + } + + if (typeof(TRequest) == typeof(APIGatewayProxyRequest)) + { + return (TRequest)(object)new APIGatewayProxyRequest + { + Body = duckRequest.Body, + Headers = duckRequest.Headers, + Path = duckRequest.Path, + HttpMethod = duckRequest.HttpMethod, + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + HttpMethod = duckRequest.HttpMethod + }, + QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }; + } + + throw new NotImplementedException( + $"Unknown request type: {typeof(TRequest).FullName}"); + } + + public static async Task SerializeToJson(HttpRequestMessage request) + { + var lambdaRequest = await ConvertToLambdaRequest(request); + string translatedRequestJson = typeof(TRequest) switch { var t when t == typeof(ApplicationLoadBalancerRequest) => JsonSerializer.Serialize( - new ApplicationLoadBalancerRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }, + lambdaRequest, LambdaRequestTypeClasses.Default.ApplicationLoadBalancerRequest), var t when t == typeof(APIGatewayHttpApiV2ProxyRequest) => JsonSerializer.Serialize( - new APIGatewayHttpApiV2ProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - RawPath = duckRequest.Path, - RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext - { - Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription - { - Method = duckRequest.HttpMethod, - Path = duckRequest.Path - } - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), - RawQueryString = duckRequest.RawQuery - }, + lambdaRequest, LambdaRequestTypeClasses.Default.APIGatewayHttpApiV2ProxyRequest), var t when t == typeof(APIGatewayProxyRequest) => JsonSerializer.Serialize( - new APIGatewayProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - RequestContext = new APIGatewayProxyRequest.ProxyRequestContext - { - HttpMethod = duckRequest.HttpMethod - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }, + lambdaRequest, LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), _ => throw new NotImplementedException( $"Unknown request type: {typeof(TRequest).FullName}") @@ -110,26 +145,6 @@ public static async Task SerializeToJson(HttpRequestMessage re return translatedRequestJson; } - /// - /// Specialized Deserializer that uses the AOT Compatible - /// to deserialize common - /// Request types. - /// - public static TRequest Deserialize(string json) - { - return typeof(TRequest) switch - { - var t when t == typeof(ApplicationLoadBalancerRequest) => - JsonSerializer.Deserialize(json, LambdaRequestTypeClasses.Default.ApplicationLoadBalancerRequest), - var t when t == typeof(APIGatewayHttpApiV2ProxyRequest) => - JsonSerializer.Deserialize(json, LambdaRequestTypeClasses.Default.APIGatewayHttpApiV2ProxyRequest), - var t when t == typeof(APIGatewayProxyRequest) => - JsonSerializer.Deserialize(json, LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), - _ => throw new NotImplementedException( - $"Unknown request type: {typeof(TRequest).FullName}") - }; - } - private static async Task ReadContent(HttpRequestMessage r) { if (r.Content == null) @@ -137,10 +152,11 @@ private static async Task ReadContent(HttpRequestMessage r) return await r.Content.ReadAsStringAsync(); } + } - [JsonSourceGenerationOptions(WriteIndented = true)] - [JsonSerializable(typeof(ApplicationLoadBalancerRequest))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] + [JsonSourceGenerationOptions] + [JsonSerializable(typeof(ApplicationLoadBalancerRequest))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestAuthentication))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert))] @@ -150,14 +166,13 @@ private static async Task ReadContent(HttpRequestMessage r) [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.IAMDescription))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.CognitoIdentityDescription))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.JwtDescription))] - [JsonSerializable(typeof(APIGatewayProxyRequest))] - [JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity))] - [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert))] - [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] - [JsonSerializable(typeof(APIGatewayProxyRequest.RequestIdentity))] - internal partial class LambdaRequestTypeClasses : JsonSerializerContext - { - } + [JsonSerializable(typeof(APIGatewayProxyRequest))] + [JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity))] + [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert))] + [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] + [JsonSerializable(typeof(APIGatewayProxyRequest.RequestIdentity))] + internal partial class LambdaRequestTypeClasses : JsonSerializerContext + { } } #endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs index fe98c0cda..b65bbc07c 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs @@ -1,29 +1,64 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using Amazon.Lambda.Core; -using Amazon.Lambda.RuntimeSupport; namespace Amazon.Lambda.AspNetCoreServer.Internal; internal class SnapStartEmptyLambdaContext : ILambdaContext, ICognitoIdentity, IClientContext { - private LambdaEnvironment _lambdaEnvironment = new(); + private static Dictionary _environmentVariables = new(); + + // Copied from Amazon.Lambda.RuntimeSupport.LambdaEnvironment to avoid adding + // a reference to that project + private const string EnvVarFunctionMemorySize = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"; + private const string EnvVarFunctionName = "AWS_LAMBDA_FUNCTION_NAME"; + private const string EnvVarFunctionVersion = "AWS_LAMBDA_FUNCTION_VERSION"; + private const string EnvVarHandler = "_HANDLER"; + private const string EnvVarLogGroupName = "AWS_LAMBDA_LOG_GROUP_NAME"; + private const string EnvVarLogStreamName = "AWS_LAMBDA_LOG_STREAM_NAME"; + + + static SnapStartEmptyLambdaContext() + { + AddEnvValue(EnvVarFunctionMemorySize, "128"); + AddEnvValue(EnvVarFunctionName, "fallbackFunctionName"); + AddEnvValue(EnvVarFunctionVersion, "0"); + AddEnvValue(EnvVarLogGroupName, "fallbackLogGroup"); + AddEnvValue(EnvVarLogStreamName, "fallbackLogStream"); + } + + private static void AddEnvValue(string envName, string fallback) + { + var val = System.Environment.GetEnvironmentVariable(envName); + + val = string.IsNullOrEmpty(val) ? fallback : val; + + _environmentVariables[envName] = val; + } + + public SnapStartEmptyLambdaContext() + { + // clone the static environment variables into the local instance + foreach (var k in _environmentVariables.Keys) + Environment[k] = _environmentVariables[k]; + } + public string TraceId => string.Empty; public string AwsRequestId => string.Empty; public IClientContext ClientContext => this; - public string FunctionName => _lambdaEnvironment.FunctionName; - public string FunctionVersion => _lambdaEnvironment.FunctionVersion; + public string FunctionName => Environment[EnvVarFunctionName]; + public string FunctionVersion => Environment[EnvVarFunctionVersion]; public ICognitoIdentity Identity => this; public string InvokedFunctionArn => string.Empty; public ILambdaLogger Logger => null; - public string LogGroupName => _lambdaEnvironment.LogGroupName; - public string LogStreamName => _lambdaEnvironment.LogStreamName; - public int MemoryLimitInMB => 128; - public TimeSpan RemainingTime => TimeSpan.FromMilliseconds(100); + public string LogGroupName => Environment[EnvVarLogGroupName]; + public string LogStreamName => Environment[EnvVarLogStreamName]; + public int MemoryLimitInMB => int.Parse(Environment[EnvVarFunctionMemorySize]); + public TimeSpan RemainingTime => TimeSpan.FromSeconds(5); public string IdentityId { get; } public string IdentityPoolId { get; } public IDictionary Environment { get; } = new Dictionary(); From e5f2c8f25b765e88c2caf41452922ddd6c4b9e10 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Mon, 21 Apr 2025 14:41:42 -0700 Subject: [PATCH 14/17] Update the Minimal API extension method to use GetBeforeSnapshotRequests. --- .../38c5bace-4ca5-4f83-8094-ae6d912ca20a.json | 2 +- ...zon.Lambda.AspNetCoreServer.Hosting.csproj | 4 +- .../GetBeforeSnapshotRequestsCollector.cs | 16 ++ .../Internal/LambdaRuntimeSupportServer.cs | 50 +++-- ...tartExecuteRequestsBeforeSnapshotHelper.cs | 142 -------------- .../ServiceCollectionExtensions.cs | 53 ++---- .../AbstractAspNetCoreFunction.cs | 63 ++++--- .../Internal/HttpRequestMessageConverter.cs | 121 ++++++++++++ .../Internal/HttpRequestMessageSerializer.cs | 178 ------------------ .../Internal/SnapStartEmptyLambdaContext.cs | 1 - .../Amazon.Lambda.RuntimeSupport.csproj | 7 - .../Client/InvocationRequest.cs | 7 - .../AddAWSLambdaBeforeSnapshotRequestTests.cs | 2 +- .../TestApiGatewayHttpApiV2Calls.cs | 16 +- .../Controllers/SnapStartController.cs | 24 --- .../TestWebApp/HttpApiV2LambdaFunction.cs | 13 -- 16 files changed, 233 insertions(+), 466 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs delete mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs create mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs delete mode 100644 Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs delete mode 100644 Libraries/test/TestWebApp/Controllers/SnapStartController.cs diff --git a/.autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json b/.autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json index 7a40133d6..080d8db06 100644 --- a/.autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json +++ b/.autover/changes/38c5bace-4ca5-4f83-8094-ae6d912ca20a.json @@ -4,7 +4,7 @@ "Name": "Amazon.Lambda.AspNetCoreServer.Hosting", "Type": "Patch", "ChangelogMessages": [ - "Add AddAWSLambdaBeforeSnapshotRequest to support warming up the asp.net/lambda pipelines automatically during BeforeSnapshot callback." + "Add overrideable method GetBeforeSnapshotRequests() and AddAWSLambdaBeforeSnapshotRequest() extension method to support warming up the asp.net/lambda pipelines automatically during BeforeSnapshot callback." ] } ] diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj index dc756ec7d..d0c71e18b 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj @@ -27,8 +27,6 @@ - - - + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs new file mode 100644 index 000000000..9faad9e4b --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; + +/// +/// Helper class for storing Requests for +/// +/// +internal class GetBeforeSnapshotRequestsCollector +{ + public HttpRequestMessage? Requests { get; set; } +} + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index ab075da2e..776e9f847 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -17,10 +17,6 @@ public abstract class LambdaRuntimeSupportServer : LambdaServer { private readonly IServiceProvider _serviceProvider; - #if NET8_0_OR_GREATER - private readonly LambdaSnapstartExecuteRequestsBeforeSnapshotHelper _snapstartInitHelper; - #endif - internal ILambdaSerializer Serializer; /// @@ -31,10 +27,6 @@ public LambdaRuntimeSupportServer(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - #if NET8_0_OR_GREATER - _snapstartInitHelper = _serviceProvider.GetRequiredService(); - #endif - Serializer = serviceProvider.GetRequiredService(); } @@ -51,12 +43,6 @@ public override Task StartAsync(IHttpApplication application var handlerWrapper = CreateHandlerWrapper(_serviceProvider); - #if NET8_0_OR_GREATER - - _snapstartInitHelper.RegisterInitializerRequests(handlerWrapper); - - #endif - var bootStrap = new LambdaBootstrap(handlerWrapper); return bootStrap.RunAsync(); } @@ -99,6 +85,8 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction { + private readonly IEnumerable _beforeSnapshotRequestsCollectors; + /// /// Create instances /// @@ -106,7 +94,17 @@ public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction public APIGatewayHttpApiV2MinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { + _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); + } + + #if NET8_0_OR_GREATER + protected override IEnumerable GetBeforeSnapshotRequests() + { + foreach (var collector in _beforeSnapshotRequestsCollectors) + if (collector.Requests != null) + yield return collector.Requests; } + #endif } } @@ -140,6 +138,8 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction { + private readonly IEnumerable _beforeSnapshotRequestsCollectors; + /// /// Create instances /// @@ -147,7 +147,17 @@ public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction public APIGatewayRestApiMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { + _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); } + + #if NET8_0_OR_GREATER + protected override IEnumerable GetBeforeSnapshotRequests() + { + foreach (var collector in _beforeSnapshotRequestsCollectors) + if (collector.Requests != null) + yield return collector.Requests; + } + #endif } } @@ -181,6 +191,8 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction { + private readonly IEnumerable _beforeSnapshotRequestsCollectors; + /// /// Create instances /// @@ -188,7 +200,17 @@ public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { + _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); + } + + #if NET8_0_OR_GREATER + protected override IEnumerable GetBeforeSnapshotRequests() + { + foreach (var collector in _beforeSnapshotRequestsCollectors) + if (collector.Requests != null) + yield return collector.Requests; } + #endif } } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs deleted file mode 100644 index 0a0b75f06..000000000 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Amazon.Lambda.APIGatewayEvents; -using Amazon.Lambda.ApplicationLoadBalancerEvents; -using Amazon.Lambda.AspNetCoreServer.Internal; -using Amazon.Lambda.Core; -using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.RuntimeSupport.Helpers; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.DependencyInjection; - -namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; - -#if NET8_0_OR_GREATER - -/// -/// Contains the plumbing to register a user provided inside -/// . -/// The function is meant to initialize the asp.net and lambda pipelines during -/// and improve the -/// performance gains offered by SnapStart. -/// -/// It works by construction a specialized that will intercept requests -/// and saved them inside . -/// -/// Intercepted requests are then be processed later by -/// which will route them correctly through a simulated asp.net/lambda pipeline. -/// -internal class LambdaSnapstartExecuteRequestsBeforeSnapshotHelper -{ - private readonly LambdaEventSource _lambdaEventSource; - - public LambdaSnapstartExecuteRequestsBeforeSnapshotHelper(LambdaEventSource lambdaEventSource) - { - _lambdaEventSource = lambdaEventSource; - } - - /// - public void RegisterInitializerRequests(HandlerWrapper handlerWrapper) - { - Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => - { - foreach (var req in Registrar.GetAllRequests()) - { - var json = await SnapstartHelperLambdaRequests.SerializeToJson(req, _lambdaEventSource); - - await SnapstartHelperLambdaRequests.ExecuteSnapstartInitRequests(json, times: 5, handlerWrapper); - } - }); - } - - - /// - internal static BeforeSnapstartRequestRegistrar Registrar = new(); - - internal class BeforeSnapstartRequestRegistrar - { - private readonly List>> _beforeSnapstartRequests = new(); - - - public void Register(Func> beforeSnapstartRequests) - { - _beforeSnapstartRequests.Add(beforeSnapstartRequests); - } - - internal IEnumerable GetAllRequests() - { - foreach (var batch in _beforeSnapstartRequests) - foreach (var r in batch()) - yield return r; - } - } - - private class HelperLambdaContext : ILambdaContext, ICognitoIdentity, IClientContext - { - private LambdaEnvironment _lambdaEnvironment = new (); - - public string TraceId => string.Empty; - public string AwsRequestId => string.Empty; - public IClientContext ClientContext => this; - public string FunctionName => _lambdaEnvironment.FunctionName; - public string FunctionVersion => _lambdaEnvironment.FunctionVersion; - public ICognitoIdentity Identity => this; - public string InvokedFunctionArn => string.Empty; - public ILambdaLogger Logger => null; - public string LogGroupName => _lambdaEnvironment.LogGroupName; - public string LogStreamName => _lambdaEnvironment.LogStreamName; - public int MemoryLimitInMB => 128; - public TimeSpan RemainingTime => TimeSpan.FromMilliseconds(100); - public string IdentityId { get; } - public string IdentityPoolId { get; } - public IDictionary Environment { get; } = new Dictionary(); - public IClientApplication Client { get; } - public IDictionary Custom { get; } = new Dictionary(); - } - - private static class SnapstartHelperLambdaRequests - { - public static async Task ExecuteSnapstartInitRequests(string jsonRequest, int times, HandlerWrapper handlerWrapper) - { - var dummyRequest = new InvocationRequest( - new MemoryStream(Encoding.UTF8.GetBytes(jsonRequest)), - new HelperLambdaContext()); - - for (var i = 0; i < times; i++) - { - try - { - _ = await handlerWrapper.Handler.Invoke(dummyRequest); - } - catch (Exception e) - { - Console.WriteLine("StartAsync: " + e.Message + e.StackTrace); - } - } - } - - public static async Task SerializeToJson(HttpRequestMessage request, LambdaEventSource lambdaType) - { - var result = lambdaType switch - { - LambdaEventSource.ApplicationLoadBalancer => - await HttpRequestMessageSerializer.SerializeToJson(request), - LambdaEventSource.HttpApi => - await HttpRequestMessageSerializer.SerializeToJson(request), - LambdaEventSource.RestApi => - await HttpRequestMessageSerializer.SerializeToJson(request), - _ => throw new NotImplementedException( - $"Unknown {nameof(LambdaEventSource)}: {Enum.GetName(lambdaType)}") - }; - - return result; - } - } -} -#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 000f970aa..b43dae555 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -90,20 +90,23 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser #if NET8_0_OR_GREATER /// - /// Adds a function meant to initialize the asp.net and lambda pipelines during - /// improving the performance gains offered by SnapStart. + /// Adds a > that will be used to invoke + /// Routes in your lambda function in order to initialize the ASP.NET Core and Lambda pipelines + /// during . This improves the performance gains + /// offered by SnapStart. /// - /// Pass a function with one or more s that will be used to invoke - /// Routes in your lambda function. The returned must have a relative + /// The returned must have a relative /// . /// . /// Be aware that this will invoke your applications function handler code - /// multiple times. Additionally, it uses a mock - /// which may not be fully populated. + /// multiple times so that .NET runtime sees this code is a hot path and should be optimized. + /// + /// When the function handler is called as part of SnapStart warm up, the instance will use a + /// mock , which will not be fully populated. /// /// This method automatically registers with . /// - /// If SnapStart is not enabled, then this method is ignored and is never invoked. + /// This method can be called multiple times to register additional urls. /// /// Example: /// @@ -115,9 +118,9 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi); /// /// // Initialize asp.net pipeline before Snapshot - /// builder.Services.AddAWSLambdaBeforeSnapshotRequest(() => [ + /// builder.Services.AddAWSLambdaBeforeSnapshotRequest( /// new HttpRequestMessage(HttpMethod.Get, "/test") - /// }); + /// ); /// /// var app = builder.Build(); /// @@ -127,29 +130,18 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// ]]> /// /// - #else - /// - /// Snapstart requires your application to target .NET 8 or above. - /// - #endif - public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, Func> beforeSnapStartRequests) + /// + /// + public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, HttpRequestMessage beforeSnapStartRequest) { - #if NET8_0_OR_GREATER - LambdaSnapstartExecuteRequestsBeforeSnapshotHelper.Registrar.Register(beforeSnapStartRequests); - #endif + services.AddSingleton(new GetBeforeSnapshotRequestsCollector + { + Requests = beforeSnapStartRequest + }); return services; } - - /// - public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, Func beforeSnapStartRequest) - { - return AddAWSLambdaBeforeSnapshotRequest(services, - () => new List - { - beforeSnapStartRequest() - }); - } + #endif private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSource eventSource, Action? configure, out HostingOptions? hostingOptions) { @@ -174,11 +166,6 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc Utilities.EnsureLambdaServerRegistered(services, serverType); - #if NET8_0_OR_GREATER - // register a LambdaSnapStartInitializerHttpMessageHandler - services.AddSingleton(new LambdaSnapstartExecuteRequestsBeforeSnapshotHelper(eventSource)); - #endif - return true; } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index 5fa1e50db..49a537345 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -122,6 +122,8 @@ protected AbstractAspNetCoreFunction(IServiceProvider hostedServices) _hostServices = hostedServices; _server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer; _logger = ActivatorUtilities.CreateInstance>>(this._hostServices); + + AddRegisterBeforeSnapshot(); } /// @@ -256,7 +258,7 @@ protected virtual IHostBuilder CreateHostBuilder() #if NET8_0_OR_GREATER /// /// Return one or more s that will be used to invoke - /// Routes in your lambda function in order to initialize the asp.net and lambda pipelines + /// Routes in your lambda function in order to initialize the ASP.NET Core and Lambda pipelines /// during , /// improving the performance gains offered by SnapStart. /// @@ -301,32 +303,8 @@ private protected bool IsStarted } } - /// - /// Should be called in the derived constructor - /// - protected void Start() + private void AddRegisterBeforeSnapshot() { - var builder = CreateHostBuilder(); - builder.ConfigureServices(services => - { - Utilities.EnsureLambdaServerRegistered(services); - }); - - var host = builder.Build(); - PostCreateHost(host); - - host.Start(); - this._hostServices = host.Services; - - _server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer; - if (_server == null) - { - throw new Exception("Failed to find the Lambda implementation for the IServer interface in the IServiceProvider for the Host. This happens if UseLambdaServer was " + - "not called when constructing the IWebHostBuilder. If CreateHostBuilder was overridden it is recommended that ConfigureWebHostLambdaDefaults should be used " + - "instead of ConfigureWebHostDefaults to make sure the property Lambda services are registered."); - } - _logger = ActivatorUtilities.CreateInstance>>(this._hostServices); - #if NET8_0_OR_GREATER Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => @@ -337,9 +315,12 @@ protected void Start() { var invokeTimes = 5; - var request = await HttpRequestMessageSerializer.ConvertToLambdaRequest(httpRequest); + var request = await HttpRequestMessageConverter.ConvertToLambdaRequest(httpRequest); InvokeFeatures features = new InvokeFeatures(); + (features as IItemsFeature).Items = new Dictionary(); + (features as IServiceProvidersFeature).RequestServices = _hostServices; + MarshallRequest(features, request, new SnapStartEmptyLambdaContext()); var context = CreateContext(features); @@ -354,7 +335,35 @@ protected void Start() }); #endif + } + + /// + /// Should be called in the derived constructor + /// + protected void Start() + { + var builder = CreateHostBuilder(); + builder.ConfigureServices(services => + { + Utilities.EnsureLambdaServerRegistered(services); + }); + + var host = builder.Build(); + PostCreateHost(host); + + host.Start(); + this._hostServices = host.Services; + + _server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer; + if (_server == null) + { + throw new Exception("Failed to find the Lambda implementation for the IServer interface in the IServiceProvider for the Host. This happens if UseLambdaServer was " + + "not called when constructing the IWebHostBuilder. If CreateHostBuilder was overridden it is recommended that ConfigureWebHostLambdaDefaults should be used " + + "instead of ConfigureWebHostDefaults to make sure the property Lambda services are registered."); + } + _logger = ActivatorUtilities.CreateInstance>>(this._hostServices); + AddRegisterBeforeSnapshot(); } /// diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs new file mode 100644 index 000000000..285fb3898 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs @@ -0,0 +1,121 @@ +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Microsoft.AspNetCore.Identity.Data; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; + +namespace Amazon.Lambda.AspNetCoreServer.Internal +{ + /// + /// Helper class for converting a to a known + /// lambda request type like: , + /// , or . + /// + /// Object is returned as a serialized string. + /// + /// This is intended for internal use to support SnapStart initialization. Not all properties + /// may be full set. + /// + public class HttpRequestMessageConverter + { + private static readonly Uri _baseUri = new Uri("http://localhost"); + + public static async Task ConvertToLambdaRequest(HttpRequestMessage request) + { + if (null == request.RequestUri) + { + throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be set.", nameof(request)); + } + + if (request.RequestUri.IsAbsoluteUri) + { + throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be relative.", nameof(request)); + } + + // make request absolut (relative to localhost) otherwise parsing the query will not work + request.RequestUri = new Uri(_baseUri, request.RequestUri); + + + var body = await ReadContent(request); + var headers = request.Headers + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.FirstOrDefault(), + StringComparer.OrdinalIgnoreCase); + var httpMethod = request.Method.ToString(); + var path = "/" + _baseUri.MakeRelativeUri(request.RequestUri); + var rawQuery = request.RequestUri?.Query; + var query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query); + + if (typeof(TRequest) == typeof(ApplicationLoadBalancerRequest)) + { + return (TRequest)(object) new ApplicationLoadBalancerRequest + { + Body = body, + Headers = headers, + Path = path, + HttpMethod = httpMethod, + QueryStringParameters = query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }; + } + + if (typeof(TRequest) == typeof(APIGatewayHttpApiV2ProxyRequest)) + { + return (TRequest)(object)new APIGatewayHttpApiV2ProxyRequest + { + Body = body, + Headers = headers, + RawPath = path, + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = httpMethod, + Path = path + } + }, + QueryStringParameters = query?.ToDictionary(k => k.Key, v => v.Value.ToString()), + RawQueryString = rawQuery + }; + } + + if (typeof(TRequest) == typeof(APIGatewayProxyRequest)) + { + return (TRequest)(object)new APIGatewayProxyRequest + { + Body = body, + Headers = headers, + Path = path, + HttpMethod = httpMethod, + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + HttpMethod = httpMethod, + Path = path + }, + QueryStringParameters = query?.ToDictionary(k => k.Key, v => v.Value.ToString()) + }; + } + + throw new NotImplementedException( + $"Unknown request type: {typeof(TRequest).FullName}"); + } + + private static async Task ReadContent(HttpRequestMessage r) + { + if (r.Content == null) + return string.Empty; + + return await r.Content.ReadAsStringAsync(); + } + } +} +#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs deleted file mode 100644 index f4904513d..000000000 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageSerializer.cs +++ /dev/null @@ -1,178 +0,0 @@ -#if NET8_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Amazon.Lambda.APIGatewayEvents; -using Amazon.Lambda.ApplicationLoadBalancerEvents; -using Microsoft.AspNetCore.Identity.Data; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; - -namespace Amazon.Lambda.AspNetCoreServer.Internal -{ - /// - /// Helper class for converting a to a known - /// lambda request type like: , - /// , or . - /// - /// Object is returned as a serialized string. - /// - /// This is intended for internal use to support Snapstart initialization. Not all properties - /// may be full set. - /// - public class HttpRequestMessageSerializer - { - private static readonly Uri _baseUri = new Uri("http://localhost"); - - internal class DuckRequest - { - public string Body { get; set; } - public Dictionary Headers { get; set; } = new(); - public string HttpMethod { get; set; } - public string Path { get; set; } - public string RawQuery { get; set; } - public Dictionary Query { get; set; } = new(); - } - - public static async Task ConvertToLambdaRequest(HttpRequestMessage request) - { - if (null == request.RequestUri) - { - throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be set.", nameof(request)); - } - - if (request.RequestUri.IsAbsoluteUri) - { - throw new ArgumentException($"{nameof(HttpRequestMessage.RequestUri)} must be relative.", nameof(request)); - } - - // make request absolut (relative to localhost) otherwise parsing the query will not work - request.RequestUri = new Uri(_baseUri, request.RequestUri); - - var duckRequest = new DuckRequest - { - Body = await ReadContent(request), - Headers = request.Headers - .ToDictionary( - kvp => kvp.Key, - kvp => kvp.Value.FirstOrDefault(), - StringComparer.OrdinalIgnoreCase), - HttpMethod = request.Method.ToString(), - Path = "/" + _baseUri.MakeRelativeUri(request.RequestUri), - RawQuery = request.RequestUri?.Query, - Query = QueryHelpers.ParseNullableQuery(request.RequestUri?.Query) - }; - - if (typeof(TRequest) == typeof(ApplicationLoadBalancerRequest)) - { - return (TRequest)(object) new ApplicationLoadBalancerRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }; - } - - if (typeof(TRequest) == typeof(APIGatewayHttpApiV2ProxyRequest)) - { - return (TRequest)(object)new APIGatewayHttpApiV2ProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - RawPath = duckRequest.Path, - RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext - { - Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription - { - Method = duckRequest.HttpMethod, - Path = duckRequest.Path - } - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()), - RawQueryString = duckRequest.RawQuery - }; - } - - if (typeof(TRequest) == typeof(APIGatewayProxyRequest)) - { - return (TRequest)(object)new APIGatewayProxyRequest - { - Body = duckRequest.Body, - Headers = duckRequest.Headers, - Path = duckRequest.Path, - HttpMethod = duckRequest.HttpMethod, - RequestContext = new APIGatewayProxyRequest.ProxyRequestContext - { - HttpMethod = duckRequest.HttpMethod - }, - QueryStringParameters = duckRequest.Query?.ToDictionary(k => k.Key, v => v.Value.ToString()) - }; - } - - throw new NotImplementedException( - $"Unknown request type: {typeof(TRequest).FullName}"); - } - - public static async Task SerializeToJson(HttpRequestMessage request) - { - var lambdaRequest = await ConvertToLambdaRequest(request); - - string translatedRequestJson = typeof(TRequest) switch - { - var t when t == typeof(ApplicationLoadBalancerRequest) => - JsonSerializer.Serialize( - lambdaRequest, - LambdaRequestTypeClasses.Default.ApplicationLoadBalancerRequest), - var t when t == typeof(APIGatewayHttpApiV2ProxyRequest) => - JsonSerializer.Serialize( - lambdaRequest, - LambdaRequestTypeClasses.Default.APIGatewayHttpApiV2ProxyRequest), - var t when t == typeof(APIGatewayProxyRequest) => - JsonSerializer.Serialize( - lambdaRequest, - LambdaRequestTypeClasses.Default.APIGatewayProxyRequest), - _ => throw new NotImplementedException( - $"Unknown request type: {typeof(TRequest).FullName}") - }; - - return translatedRequestJson; - } - - private static async Task ReadContent(HttpRequestMessage r) - { - if (r.Content == null) - return string.Empty; - - return await r.Content.ReadAsStringAsync(); - } - } - - [JsonSourceGenerationOptions] - [JsonSerializable(typeof(ApplicationLoadBalancerRequest))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestAuthentication))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ProxyRequestClientCert))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.ClientCertValidity))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.HttpDescription))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.IAMDescription))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.CognitoIdentityDescription))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest.AuthorizerDescription.JwtDescription))] - [JsonSerializable(typeof(APIGatewayProxyRequest))] - [JsonSerializable(typeof(APIGatewayProxyRequest.ClientCertValidity))] - [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestClientCert))] - [JsonSerializable(typeof(APIGatewayProxyRequest.ProxyRequestContext))] - [JsonSerializable(typeof(APIGatewayProxyRequest.RequestIdentity))] - internal partial class LambdaRequestTypeClasses : JsonSerializerContext - { - } -} -#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs index b65bbc07c..3cad35617 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs @@ -16,7 +16,6 @@ internal class SnapStartEmptyLambdaContext : ILambdaContext, ICognitoIdentity, I private const string EnvVarFunctionMemorySize = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"; private const string EnvVarFunctionName = "AWS_LAMBDA_FUNCTION_NAME"; private const string EnvVarFunctionVersion = "AWS_LAMBDA_FUNCTION_VERSION"; - private const string EnvVarHandler = "_HANDLER"; private const string EnvVarLogGroupName = "AWS_LAMBDA_LOG_GROUP_NAME"; private const string EnvVarLogStreamName = "AWS_LAMBDA_LOG_STREAM_NAME"; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj index 1f72bebfc..47ce7f796 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj @@ -16,13 +16,6 @@ 9.0 - - - ..\..\..\buildtools\public.snk - true - - Exe $(DefineConstants);ExecutableOutputType diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs index 7412fa1ab..93d5bf151 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationRequest.cs @@ -35,13 +35,6 @@ public class InvocationRequest : IDisposable internal InvocationRequest() { } - /// > - public InvocationRequest(Stream inputStream, ILambdaContext lambdaContext) - { - InputStream = inputStream; - LambdaContext = lambdaContext; - } - public void Dispose() { InputStream?.Dispose(); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs index 1f761bfcd..b4419b1a7 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -30,7 +30,7 @@ public async Task VerifyCallbackIsInvoked(LambdaEventSource hostingType) builder.Services.AddAWSLambdaHosting(hostingType); // Initialize asp.net pipeline before Snapshot - builder.Services.AddAWSLambdaBeforeSnapshotRequest(() => + builder.Services.AddAWSLambdaBeforeSnapshotRequest( new HttpRequestMessage(HttpMethod.Get, "/test") ); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs index b91255b5e..8b6fe973c 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs @@ -15,7 +15,7 @@ using Newtonsoft.Json.Linq; using TestWebApp; -using TestWebApp.Controllers; + using Xunit; @@ -282,20 +282,6 @@ public async Task TestTraceIdSetFromLambdaContext() } } - #if NET8_0_OR_GREATER - /// - /// Verifies that is invoked during startup. - /// - /// - [Fact] - public void TestSnapstartInititalizaton() - { - var lambdaFunction = new TestWebApp.HttpV2LambdaFunction(); - - Assert.True(SnapStartController.Invoked); - } - #endif - private async Task InvokeAPIGatewayRequest(string fileName, bool configureApiToReturnExceptionDetail = false) { return await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), GetRequestContent(fileName), configureApiToReturnExceptionDetail); diff --git a/Libraries/test/TestWebApp/Controllers/SnapStartController.cs b/Libraries/test/TestWebApp/Controllers/SnapStartController.cs deleted file mode 100644 index 58ffdc448..000000000 --- a/Libraries/test/TestWebApp/Controllers/SnapStartController.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using Microsoft.AspNetCore.Mvc; - -namespace TestWebApp.Controllers; - -[Route("api/[controller]")] -public class SnapStartController -{ - /// - /// Set when is invoked - /// - public static bool Invoked { get; set; } - - - [HttpGet] - public string Get() - { - Invoked = true; - - return "Invoked set to true"; - } -} diff --git a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs index 0561b2b84..fa97a6952 100644 --- a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs +++ b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs @@ -1,20 +1,7 @@ -using System.Collections.Generic; -using System.Net.Http; -using System; using Amazon.Lambda.AspNetCoreServer; namespace TestWebApp { public class HttpV2LambdaFunction : APIGatewayHttpApiV2ProxyFunction { -#if NET8_0_OR_GREATER - protected override IEnumerable GetBeforeSnapshotRequests() => - [ - new HttpRequestMessage - { - RequestUri = new Uri("/api/Snapstart"), - Method = HttpMethod.Get - } - ]; -#endif } } From d5780f33e8bb0024de93f8f4288c24bce09e35e8 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Tue, 22 Apr 2025 15:41:21 -0700 Subject: [PATCH 15/17] Add additional test for asp.net workloads --- ...Amazon.Lambda.AspNetCoreServer.Test.csproj | 1 + .../TestApiGatewayHttpApiV2Calls.cs | 45 ++++++++++++++++++- .../Controllers/SnapStartController.cs | 24 ++++++++++ .../TestWebApp/HttpApiV2LambdaFunction.cs | 9 ++++ Libraries/test/TestWebApp/Middleware.cs | 10 +++-- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 Libraries/test/TestWebApp/Controllers/SnapStartController.cs diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj index 0ec5b957c..9ace52777 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj @@ -41,6 +41,7 @@ + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs index 8b6fe973c..4781b4a26 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; @@ -9,13 +9,15 @@ using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; using Amazon.Lambda.TestUtilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using TestWebApp; - +using TestWebApp.Controllers; using Xunit; @@ -282,6 +284,30 @@ public async Task TestTraceIdSetFromLambdaContext() } } + #if NET8_0_OR_GREATER + /// + /// Verifies that is invoked during startup. + /// + /// + [Fact] + public async Task TestSnapStartInitialization() + { + using var e1 = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", nameof(TestSnapStartInitialization)); + using var e2 = new EnvironmentVariableHelper("AWS_LAMBDA_INITIALIZATION_TYPE", "snap-start"); + using var e3 = new EnvironmentVariableHelper("AWS_LAMBDA_RUNTIME_API", "localhost:123"); + + var t = LambdaBootstrapBuilder.Create( + new TestWebApp.HttpV2LambdaFunction().FunctionHandlerAsync, + new DefaultLambdaJsonSerializer()) + .Build() + .RunAsync(); + + await Task.Delay(1000); + + Assert.True(SnapStartController.Invoked); + } + #endif + private async Task InvokeAPIGatewayRequest(string fileName, bool configureApiToReturnExceptionDetail = false) { return await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), GetRequestContent(fileName), configureApiToReturnExceptionDetail); @@ -310,4 +336,19 @@ private string GetRequestContent(string fileName) return requestStr; } } + + public class EnvironmentVariableHelper : IDisposable + { + private string _name; + private string? _oldValue; + public EnvironmentVariableHelper(string name, string value) + { + _name = name; + _oldValue = Environment.GetEnvironmentVariable(name); + + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() => Environment.SetEnvironmentVariable(_name, _oldValue); + } } diff --git a/Libraries/test/TestWebApp/Controllers/SnapStartController.cs b/Libraries/test/TestWebApp/Controllers/SnapStartController.cs new file mode 100644 index 000000000..58ffdc448 --- /dev/null +++ b/Libraries/test/TestWebApp/Controllers/SnapStartController.cs @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc; + +namespace TestWebApp.Controllers; + +[Route("api/[controller]")] +public class SnapStartController +{ + /// + /// Set when is invoked + /// + public static bool Invoked { get; set; } + + + [HttpGet] + public string Get() + { + Invoked = true; + + return "Invoked set to true"; + } +} diff --git a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs index fa97a6952..fa525b113 100644 --- a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs +++ b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs @@ -1,7 +1,16 @@ +using System.Collections.Generic; +using System.Net.Http; +using System; using Amazon.Lambda.AspNetCoreServer; namespace TestWebApp { public class HttpV2LambdaFunction : APIGatewayHttpApiV2ProxyFunction { +#if NET8_0_OR_GREATER + protected override IEnumerable GetBeforeSnapshotRequests() => + [ + new HttpRequestMessage(HttpMethod.Get, "api/SnapStart") + ]; +#endif } } diff --git a/Libraries/test/TestWebApp/Middleware.cs b/Libraries/test/TestWebApp/Middleware.cs index 6a07d9857..95b2ee3d2 100644 --- a/Libraries/test/TestWebApp/Middleware.cs +++ b/Libraries/test/TestWebApp/Middleware.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.Core; +using Amazon.Lambda.Core; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; @@ -23,8 +23,12 @@ public async Task Invoke(HttpContext context) context.Response.OnStarting(x => { - var lambdaContext = context.Items["LambdaContext"] as ILambdaContext; - lambdaContext?.Logger.LogLine("OnStarting Called"); + if (context.Items.ContainsKey("LambdaContext")) + { + var lambdaContext = context.Items["LambdaContext"] as ILambdaContext; + lambdaContext?.Logger.LogLine("OnStarting Called"); + } + return Task.FromResult(0); }, context); From 6d2776636359598517cde456f8479030ef5efc69 Mon Sep 17 00:00:00 2001 From: philip pittle Date: Thu, 24 Apr 2025 15:49:45 -0700 Subject: [PATCH 16/17] improve documentation --- .../GetBeforeSnapshotRequestsCollector.cs | 5 ++-- .../Internal/LambdaRuntimeSupportServer.cs | 24 ++++++++++++++----- .../ServiceCollectionExtensions.cs | 7 +++--- .../AbstractAspNetCoreFunction.cs | 3 ++- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs index 9faad9e4b..8cbb12d8f 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs @@ -5,12 +5,13 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +#if NET8_0_OR_GREATER /// /// Helper class for storing Requests for /// /// internal class GetBeforeSnapshotRequestsCollector { - public HttpRequestMessage? Requests { get; set; } + public HttpRequestMessage? Request { get; set; } } - +#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index 776e9f847..c862b790c 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -85,7 +85,9 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction { + #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; + #endif /// /// Create instances @@ -94,15 +96,17 @@ public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction public APIGatewayHttpApiV2MinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { + #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); + #endif } #if NET8_0_OR_GREATER protected override IEnumerable GetBeforeSnapshotRequests() { foreach (var collector in _beforeSnapshotRequestsCollectors) - if (collector.Requests != null) - yield return collector.Requests; + if (collector.Request != null) + yield return collector.Request; } #endif } @@ -138,7 +142,9 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction { + #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; + #endif /// /// Create instances @@ -147,15 +153,17 @@ public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction public APIGatewayRestApiMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { + #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); + #endif } #if NET8_0_OR_GREATER protected override IEnumerable GetBeforeSnapshotRequests() { foreach (var collector in _beforeSnapshotRequestsCollectors) - if (collector.Requests != null) - yield return collector.Requests; + if (collector.Request != null) + yield return collector.Request; } #endif } @@ -191,7 +199,9 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction { + #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; + #endif /// /// Create instances @@ -200,15 +210,17 @@ public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { + #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); + #endif } #if NET8_0_OR_GREATER protected override IEnumerable GetBeforeSnapshotRequests() { foreach (var collector in _beforeSnapshotRequestsCollectors) - if (collector.Requests != null) - yield return collector.Requests; + if (collector.Request != null) + yield return collector.Request; } #endif } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index b43dae555..645b5ba91 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -95,8 +95,9 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// during . This improves the performance gains /// offered by SnapStart. /// - /// The returned must have a relative - /// . + /// must have a relative + /// and the only supports + /// text based payload. /// . /// Be aware that this will invoke your applications function handler code /// multiple times so that .NET runtime sees this code is a hot path and should be optimized. @@ -136,7 +137,7 @@ public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IService { services.AddSingleton(new GetBeforeSnapshotRequestsCollector { - Requests = beforeSnapStartRequest + Request = beforeSnapStartRequest }); return services; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index 49a537345..27e09e7a6 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -263,7 +263,8 @@ protected virtual IHostBuilder CreateHostBuilder() /// improving the performance gains offered by SnapStart. /// /// The returned s must have a relative - /// . + /// and the only supports + /// text based payload. /// . /// Be aware that this will invoke your applications function handler code /// multiple times. Additionally, it uses a mock From ea8e49292873ead21a051b3076696f1adeec782d Mon Sep 17 00:00:00 2001 From: philip pittle Date: Thu, 24 Apr 2025 16:07:38 -0700 Subject: [PATCH 17/17] impvoe unit tests --- .../TestApiGatewayHttpApiV2Calls.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs index 4781b4a26..1b844bf1e 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs @@ -6,6 +6,7 @@ using System.Net; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; @@ -294,15 +295,21 @@ public async Task TestSnapStartInitialization() { using var e1 = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", nameof(TestSnapStartInitialization)); using var e2 = new EnvironmentVariableHelper("AWS_LAMBDA_INITIALIZATION_TYPE", "snap-start"); - using var e3 = new EnvironmentVariableHelper("AWS_LAMBDA_RUNTIME_API", "localhost:123"); - var t = LambdaBootstrapBuilder.Create( + var cts = new CancellationTokenSource(); + + using var bootstrap = LambdaBootstrapBuilder.Create( new TestWebApp.HttpV2LambdaFunction().FunctionHandlerAsync, new DefaultLambdaJsonSerializer()) - .Build() - .RunAsync(); + .ConfigureOptions(opt => opt.RuntimeApiEndpoint = "localhost:123") + .Build(); + + _ = bootstrap.RunAsync(cts.Token); + + // allow some time for Bootstrap to initialize in background + await Task.Delay(100, cts.Token); - await Task.Delay(1000); + await cts.CancelAsync(); Assert.True(SnapStartController.Invoked); }