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..080d8db06 --- /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 overrideable method GetBeforeSnapshotRequests() and AddAWSLambdaBeforeSnapshotRequest() extension method to support warming up the asp.net/lambda pipelines automatically during BeforeSnapshot callback." + ] + } + ] +} \ No newline at end of file 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/GetBeforeSnapshotRequestsCollector.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs new file mode 100644 index 000000000..8cbb12d8f --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs @@ -0,0 +1,17 @@ +// 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; + +#if NET8_0_OR_GREATER +/// +/// Helper class for storing Requests for +/// +/// +internal class GetBeforeSnapshotRequestsCollector +{ + 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 05493e244..c862b790c 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -1,9 +1,8 @@ -using Amazon.Lambda.AspNetCoreServer.Internal; +using System.Diagnostics.CodeAnalysis; +using Amazon.Lambda.AspNetCoreServer.Internal; using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.Serialization.SystemTextJson; using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal @@ -16,7 +15,8 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal /// public abstract class LambdaRuntimeSupportServer : LambdaServer { - IServiceProvider _serviceProvider; + private readonly IServiceProvider _serviceProvider; + internal ILambdaSerializer Serializer; /// @@ -26,6 +26,7 @@ public abstract class LambdaRuntimeSupportServer : LambdaServer public LambdaRuntimeSupportServer(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; + Serializer = serviceProvider.GetRequiredService(); } @@ -41,6 +42,7 @@ public override Task StartAsync(IHttpApplication application base.StartAsync(application, cancellationToken); var handlerWrapper = CreateHandlerWrapper(_serviceProvider); + var bootStrap = new LambdaBootstrap(handlerWrapper); return bootStrap.RunAsync(); } @@ -83,6 +85,10 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction { + #if NET8_0_OR_GREATER + private readonly IEnumerable _beforeSnapshotRequestsCollectors; + #endif + /// /// Create instances /// @@ -90,7 +96,19 @@ 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.Request != null) + yield return collector.Request; + } + #endif } } @@ -124,6 +142,10 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction { + #if NET8_0_OR_GREATER + private readonly IEnumerable _beforeSnapshotRequestsCollectors; + #endif + /// /// Create instances /// @@ -131,7 +153,19 @@ 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.Request != null) + yield return collector.Request; + } + #endif } } @@ -165,6 +199,10 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction { + #if NET8_0_OR_GREATER + private readonly IEnumerable _beforeSnapshotRequestsCollectors; + #endif + /// /// Create instances /// @@ -172,7 +210,19 @@ 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.Request != null) + yield return collector.Request; } + #endif } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 82fa10376..645b5ba91 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,62 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser return services; } + #if NET8_0_OR_GREATER + /// + /// 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. + /// + /// 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. + /// + /// 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 . + /// + /// This method can be called multiple times to register additional urls. + /// + /// Example: + /// + /// + /// "Success"); + /// + /// app.Run(); + /// ]]> + /// + /// + /// + /// + public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, HttpRequestMessage beforeSnapStartRequest) + { + services.AddSingleton(new GetBeforeSnapshotRequestsCollector + { + Request = beforeSnapStartRequest + }); + + return services; + } + #endif + private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSource eventSource, Action? configure, out HostingOptions? hostingOptions) { hostingOptions = null; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index 2a3ed020c..27e09e7a6 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; @@ -120,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(); } /// @@ -251,6 +255,47 @@ 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 Core and Lambda pipelines + /// during , + /// 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 + /// 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 GetBeforeSnapshotRequests() => + Enumerable.Empty(); + #endif + private protected bool IsStarted { get @@ -259,6 +304,40 @@ private protected bool IsStarted } } + private void AddRegisterBeforeSnapshot() + { + #if NET8_0_OR_GREATER + + Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => + { + var beforeSnapstartRequests = GetBeforeSnapshotRequests(); + + foreach (var httpRequest in beforeSnapstartRequests) + { + var invokeTimes = 5; + + 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); + + for (var i = 0; i < invokeTimes; i++) + { + var lambdaContext = new SnapStartEmptyLambdaContext(); + + await ProcessRequest(lambdaContext, context, features); + } + } + }); + + #endif + } + /// /// Should be called in the derived constructor /// @@ -284,6 +363,8 @@ protected void Start() "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/SnapStartEmptyLambdaContext.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs new file mode 100644 index 000000000..3cad35617 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs @@ -0,0 +1,66 @@ +// 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; + +namespace Amazon.Lambda.AspNetCoreServer.Internal; + +internal class SnapStartEmptyLambdaContext : ILambdaContext, ICognitoIdentity, IClientContext +{ + 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 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 => Environment[EnvVarFunctionName]; + public string FunctionVersion => Environment[EnvVarFunctionVersion]; + public ICognitoIdentity Identity => this; + public string InvokedFunctionArn => string.Empty; + public ILambdaLogger Logger => null; + 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(); + public IClientApplication Client { get; } + public IDictionary Custom { get; } = new Dictionary(); +} 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..b4419b1a7 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -0,0 +1,59 @@ + +// 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( + new HttpRequestMessage(HttpMethod.Get, "/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); +} 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..1b844bf1e 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; @@ -6,16 +6,19 @@ using System.Net; using System.Reflection; using System.Text; +using System.Threading; 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 +285,36 @@ 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"); + + var cts = new CancellationTokenSource(); + + using var bootstrap = LambdaBootstrapBuilder.Create( + new TestWebApp.HttpV2LambdaFunction().FunctionHandlerAsync, + new DefaultLambdaJsonSerializer()) + .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 cts.CancelAsync(); + + Assert.True(SnapStartController.Invoked); + } + #endif + private async Task InvokeAPIGatewayRequest(string fileName, bool configureApiToReturnExceptionDetail = false) { return await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), GetRequestContent(fileName), configureApiToReturnExceptionDetail); @@ -310,4 +343,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 596eac4b7..fa525b113 100644 --- a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs +++ b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs @@ -1,7 +1,16 @@ -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 GetBeforeSnapshotRequests() => + [ + new HttpRequestMessage(HttpMethod.Get, "api/SnapStart") + ]; +#endif } -} \ No newline at end of file +} 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);