From 69f96198b3fe21abb4e8348e49bfe15f73707a5c Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 14 May 2026 18:30:29 -0400 Subject: [PATCH 1/5] Expose ILambdaSerializer on ILambdaContext Adds an optional `Serializer` property to `ILambdaContext` (default-implemented to `null` on net8.0+, matching the existing TenantId/TraceId pattern), and has RuntimeSupport propagate the serializer registered with `HandlerWrapper` / `LambdaBootstrapBuilder.Create` through `RuntimeApiClient` to the per-invocation `LambdaContext`. User code can now reuse the configured serializer for ad-hoc serialization without re-instantiating it. --- .../6e13a012-1f93-4e55-90b5-d2dd480d086c.json | 25 +++ .../src/Amazon.Lambda.Core/ILambdaContext.cs | 10 + .../Bootstrap/HandlerWrapper.cs | 48 +++-- .../Bootstrap/LambdaBootstrap.cs | 15 +- .../Client/RuntimeApiClient.cs | 13 +- .../Context/LambdaContext.cs | 16 +- .../TestLambdaContext.cs | 7 + .../TestLambdaContextSerializerTest.cs | 37 ++++ .../LambdaContextSerializerTests.cs | 180 ++++++++++++++++++ 9 files changed, 322 insertions(+), 29 deletions(-) create mode 100644 .autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json create mode 100644 Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs diff --git a/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json b/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json new file mode 100644 index 000000000..665efc4ca --- /dev/null +++ b/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json @@ -0,0 +1,25 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Core", + "Type": "Minor", + "ChangelogMessages": [ + "Add ILambdaSerializer Serializer property to ILambdaContext (default-implemented to null on net8.0+) so user code can access the serializer registered with the runtime" + ] + }, + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Minor", + "ChangelogMessages": [ + "Expose the registered ILambdaSerializer on HandlerWrapper.Serializer and propagate it to the per-invocation ILambdaContext.Serializer" + ] + }, + { + "Name": "Amazon.Lambda.TestUtilities", + "Type": "Minor", + "ChangelogMessages": [ + "Add Serializer setter to TestLambdaContext to mirror the new ILambdaContext.Serializer property" + ] + } + ] +} diff --git a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs index 81f290408..df0ef848f 100644 --- a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs @@ -95,6 +95,16 @@ public interface ILambdaContext /// The trace id generated by Lambda for distributed tracing across AWS services. /// string TraceId { get { return string.Empty; } } + + /// + /// The Lambda serializer registered for the current invocation. When the function + /// is run via Amazon.Lambda.RuntimeSupport, this is the same + /// instance passed to + /// LambdaBootstrapBuilder.Create / HandlerWrapper.GetHandlerWrapper, + /// allowing user code to reuse it for ad-hoc serialization. Can be null + /// when the runtime did not register a serializer (e.g., raw-stream handlers). + /// + ILambdaSerializer Serializer { get { return null; } } #endif } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs index e3a74d04b..8add132e9 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs @@ -36,6 +36,14 @@ public class HandlerWrapper : IDisposable /// public LambdaBootstrapHandler Handler { get; private set; } + /// + /// The serializer registered with the wrapper, if any. Surfaced so the + /// runtime bootstrap can attach it to the per-invocation + /// , allowing user code to reuse it. + /// Null for handlers that don't take a typed input/output. + /// + public ILambdaSerializer Serializer { get; private set; } + private HandlerWrapper(LambdaBootstrapHandler handler) { Handler = handler; @@ -121,7 +129,7 @@ public static HandlerWrapper GetHandlerWrapper(Func handle TInput input = serializer.Deserialize(invocation.InputStream); await handler(input); return EmptyInvocationResponse; - }); + }) { Serializer = serializer }; } /// @@ -171,7 +179,7 @@ public static HandlerWrapper GetHandlerWrapper(Func(invocation.InputStream); await handler(input, invocation.LambdaContext); return EmptyInvocationResponse; - }); + }) { Serializer = serializer }; } /// @@ -218,7 +226,7 @@ public static HandlerWrapper GetHandlerWrapper(Func { TInput input = serializer.Deserialize(invocation.InputStream); return new InvocationResponse(await handler(input)); - }); + }) { Serializer = serializer }; } /// @@ -265,7 +273,7 @@ public static HandlerWrapper GetHandlerWrapper(Func(invocation.InputStream); return new InvocationResponse(await handler(input, invocation.LambdaContext)); - }); + }) { Serializer = serializer }; } /// @@ -278,7 +286,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(); @@ -300,7 +308,7 @@ public static HandlerWrapper GetHandlerWrapper(Func> hand /// A HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(invocation.InputStream); @@ -322,7 +330,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); @@ -345,7 +353,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(invocation.LambdaContext); @@ -367,7 +375,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(invocation.InputStream, invocation.LambdaContext); @@ -389,7 +397,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); @@ -449,7 +457,7 @@ public static HandlerWrapper GetHandlerWrapper(Action handler, I TInput input = serializer.Deserialize(invocation.InputStream); handler(input); return Task.FromResult(EmptyInvocationResponse); - }); + }) { Serializer = serializer }; } /// @@ -499,7 +507,7 @@ public static HandlerWrapper GetHandlerWrapper(Action(invocation.InputStream); handler(input, invocation.LambdaContext); return Task.FromResult(EmptyInvocationResponse); - }); + }) { Serializer = serializer }; } /// @@ -546,7 +554,7 @@ public static HandlerWrapper GetHandlerWrapper(Func hand { TInput input = serializer.Deserialize(invocation.InputStream); return Task.FromResult(new InvocationResponse(handler(input))); - }); + }) { Serializer = serializer }; } /// @@ -593,7 +601,7 @@ public static HandlerWrapper GetHandlerWrapper(Func(invocation.InputStream); return Task.FromResult(new InvocationResponse(handler(input, invocation.LambdaContext))); - }); + }) { Serializer = serializer }; } /// @@ -606,7 +614,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(); @@ -628,7 +636,7 @@ public static HandlerWrapper GetHandlerWrapper(Func handler, I /// A HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(invocation.InputStream); @@ -650,7 +658,7 @@ public static HandlerWrapper GetHandlerWrapper(Func ha /// A HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); @@ -673,7 +681,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(invocation.LambdaContext); @@ -695,7 +703,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(invocation.InputStream, invocation.LambdaContext); @@ -717,7 +725,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index da5367cab..4acfc54cd 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -101,7 +101,7 @@ public LambdaBootstrap(LambdaBootstrapHandler handler, LambdaBootstrapOptions la /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapInitializer initializer = null) - : this(handlerWrapper.Handler, initializer) + : this(ConstructHttpClient(), handlerWrapper.Handler, initializer, ownsHttpClient: true, serializer: handlerWrapper.Serializer) { } /// @@ -111,7 +111,7 @@ public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapInitializer /// Lambda bootstrap configuration options. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapOptions lambdaBootstrapOptions, LambdaBootstrapInitializer initializer = null) - : this(handlerWrapper.Handler, lambdaBootstrapOptions, initializer) + : this(ConstructHttpClient(), handlerWrapper.Handler, initializer, ownsHttpClient: true, lambdaBootstrapOptions: lambdaBootstrapOptions, serializer: handlerWrapper.Serializer) { } /// @@ -122,7 +122,7 @@ public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapOptions lam /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(HttpClient httpClient, HandlerWrapper handlerWrapper, LambdaBootstrapInitializer initializer = null) - : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false) + : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false, serializer: handlerWrapper.Serializer) { } /// @@ -133,7 +133,7 @@ public LambdaBootstrap(HttpClient httpClient, HandlerWrapper handlerWrapper, Lam /// Lambda bootstrap configuration options. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. public LambdaBootstrap(HttpClient httpClient, HandlerWrapper handlerWrapper, LambdaBootstrapOptions lambdaBootstrapOptions, LambdaBootstrapInitializer initializer = null) - : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false, lambdaBootstrapOptions: lambdaBootstrapOptions) + : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false, lambdaBootstrapOptions: lambdaBootstrapOptions, serializer: handlerWrapper.Serializer) { } /// @@ -170,7 +170,8 @@ internal LambdaBootstrap(LambdaBootstrapHandler handler, LambdaBootstrapInitiali /// Get configuration to check if Invoke is with Pre JIT or SnapStart enabled /// Lambda bootstrap configuration options. /// - internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer, bool ownsHttpClient, LambdaBootstrapConfiguration configuration = null, LambdaBootstrapOptions lambdaBootstrapOptions = null, IEnvironmentVariables environmentVariables = null) + /// The Lambda serializer to expose on the per-invocation . May be null. + internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer, bool ownsHttpClient, LambdaBootstrapConfiguration configuration = null, LambdaBootstrapOptions lambdaBootstrapOptions = null, IEnvironmentVariables environmentVariables = null, Amazon.Lambda.Core.ILambdaSerializer serializer = null) { if (ownsHttpClient && httpClient == null) { @@ -183,7 +184,9 @@ internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, _initializer = initializer; _httpClient.Timeout = RuntimeApiHttpTimeout; _environmentVariables = environmentVariables ?? new SystemEnvironmentVariables(); - Client = new RuntimeApiClient(_environmentVariables, _httpClient, lambdaBootstrapOptions); + var runtimeApiClient = new RuntimeApiClient(_environmentVariables, _httpClient, lambdaBootstrapOptions); + runtimeApiClient.Serializer = serializer; + Client = runtimeApiClient; _configuration = configuration ?? LambdaBootstrapConfiguration.GetDefaultConfiguration(_environmentVariables); _awsSdkTraceIdSetter = Utils.FindAWSSDKTraceIdSetter(_environmentVariables); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs index 39cc7d055..e71a99485 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport.Helpers; using System; using System.IO; @@ -37,6 +38,13 @@ public class RuntimeApiClient : IRuntimeApiClient internal Func ExceptionConverter { get; set; } internal LambdaEnvironment LambdaEnvironment { get; set; } + /// + /// Optional serializer attached to the per-invocation + /// in . + /// Set by from the registered . + /// + internal ILambdaSerializer Serializer { get; set; } + /// public IConsoleLoggerWriter ConsoleLogger => _consoleLoggerRedirector; @@ -100,7 +108,8 @@ public Task ReportInitializationErrorAsync(string errorType, CancellationToken c /// /// Get the next function invocation from the Runtime API as an asynchronous operation. - /// Completes when the next invocation is received. + /// Completes when the next invocation is received. The + /// (if set) is attached to the resulting . /// /// The optional cancellation token to use to stop listening for the next invocation. /// A Task representing the asynchronous operation. @@ -109,7 +118,7 @@ public async Task GetNextInvocationAsync(CancellationToken ca SwaggerResponse response = await _internalClient.NextAsync(cancellationToken); var headers = new RuntimeApiHeaders(response.Headers); - var lambdaContext = new LambdaContext(headers, LambdaEnvironment, _consoleLoggerRedirector); + var lambdaContext = new LambdaContext(headers, LambdaEnvironment, _consoleLoggerRedirector, Serializer); return new InvocationRequest { InputStream = response.Result, diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs index cca6f4e5d..4e8d1dd4f 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs @@ -31,19 +31,31 @@ internal class LambdaContext : ILambdaContext private readonly Lazy _cognitoIdentityLazy; private readonly Lazy _cognitoClientContextLazy; private readonly IConsoleLoggerWriter _consoleLogger; + private readonly ILambdaSerializer _serializer; public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IConsoleLoggerWriter consoleLogger) - : this(runtimeApiHeaders, lambdaEnvironment, new DateTimeHelper(), consoleLogger) + : this(runtimeApiHeaders, lambdaEnvironment, new DateTimeHelper(), consoleLogger, serializer: null) + { + } + + public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IConsoleLoggerWriter consoleLogger, ILambdaSerializer serializer) + : this(runtimeApiHeaders, lambdaEnvironment, new DateTimeHelper(), consoleLogger, serializer) { } public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IDateTimeHelper dateTimeHelper, IConsoleLoggerWriter consoleLogger) + : this(runtimeApiHeaders, lambdaEnvironment, dateTimeHelper, consoleLogger, serializer: null) + { + } + + public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IDateTimeHelper dateTimeHelper, IConsoleLoggerWriter consoleLogger, ILambdaSerializer serializer) { _lambdaEnvironment = lambdaEnvironment; _runtimeApiHeaders = runtimeApiHeaders; _dateTimeHelper = dateTimeHelper; _consoleLogger = consoleLogger; + _serializer = serializer; int.TryParse(_lambdaEnvironment.FunctionMemorySize, out _memoryLimitInMB); long.TryParse(_runtimeApiHeaders.DeadlineMs, out _deadlineMs); @@ -77,6 +89,8 @@ public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lamb public string TenantId => _runtimeApiHeaders.TenantId; + public ILambdaSerializer Serializer => _serializer; + internal IRuntimeApiHeaders RuntimeApiHeaders => _runtimeApiHeaders; } } diff --git a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs index e3a47d308..b472403e0 100644 --- a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs @@ -80,5 +80,12 @@ public class TestLambdaContext : ILambdaContext /// The trace id generated by Lambda for distributed tracing across AWS services. /// public string TraceId { get; set; } + + /// + /// The Lambda serializer registered for the current invocation. Tests can set this + /// to mirror the serializer that the Lambda runtime support library would attach + /// in production. + /// + public ILambdaSerializer Serializer { get; set; } } } diff --git a/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs b/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs new file mode 100644 index 000000000..678647394 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.IO; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using Xunit; + +namespace Amazon.Lambda.Tests +{ + public class TestLambdaContextSerializerTest + { + [Fact] + public void Serializer_DefaultsToNull() + { + var context = new TestLambdaContext(); + + Assert.Null(context.Serializer); + } + + [Fact] + public void Serializer_RoundTripsThroughTestContext() + { + var stub = new StubSerializer(); + var context = new TestLambdaContext { Serializer = stub }; + + ILambdaContext asInterface = context; + Assert.Same(stub, asInterface.Serializer); + } + + private sealed class StubSerializer : ILambdaSerializer + { + public T Deserialize(Stream requestStream) => default; + public void Serialize(T response, Stream responseStream) { } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs new file mode 100644 index 000000000..55db8c462 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs @@ -0,0 +1,180 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +using Amazon.Lambda.Core; +using Amazon.Lambda.Serialization.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + /// + /// Verifies that the serializer registered with a / + /// is exposed on the per-invocation + /// via . + /// + public class LambdaContextSerializerTests + { + private static readonly JsonSerializer SharedSerializer = new JsonSerializer(); + + private readonly LambdaEnvironment _lambdaEnvironment; + private readonly RuntimeApiHeaders _runtimeApiHeaders; + + public LambdaContextSerializerTests() + { + var environmentVariables = new TestEnvironmentVariables(); + _lambdaEnvironment = new LambdaEnvironment(environmentVariables); + + var headers = new Dictionary> + { + [RuntimeApiHeaders.HeaderAwsRequestId] = new[] { "request-id" }, + [RuntimeApiHeaders.HeaderInvokedFunctionArn] = new[] { "invoked-function-arn" } + }; + _runtimeApiHeaders = new RuntimeApiHeaders(headers); + } + + [Fact] + public void LambdaContext_Serializer_DefaultsToNull_WhenNotSupplied() + { + var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())); + + Assert.Null(context.Serializer); + } + + [Fact] + public void LambdaContext_Serializer_ReturnsConstructorArgument() + { + var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables()), SharedSerializer); + + Assert.Same(SharedSerializer, context.Serializer); + } + + [Fact] + public void HandlerWrapper_PocoInOut_ExposesSerializer() + { + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + input => Task.FromResult(new PocoOutput()), + SharedSerializer); + + Assert.Same(SharedSerializer, handlerWrapper.Serializer); + } + + [Fact] + public void HandlerWrapper_RawStreamOverloads_HaveNullSerializer() + { + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (Func>)((input) => Task.FromResult(new MemoryStream()))); + + Assert.Null(handlerWrapper.Serializer); + } + + [Fact] + public void HandlerWrapper_AllSerializerOverloads_PropagateSerializer() + { + // One sample per overload family (Func/Action × Task/non-Task × in/out × ILambdaContext) + // is enough — they share the same field-assignment line. This guards against future + // overloads being added without setting Serializer. + + using (var w = HandlerWrapper.GetHandlerWrapper((input) => Task.CompletedTask, SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper((input, ctx) => Task.CompletedTask, SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper( + (Func>)((input) => Task.FromResult(new MemoryStream())), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper(() => Task.FromResult(new PocoOutput()), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper( + (input, ctx) => Task.FromResult(new PocoOutput()), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper((Action)(input => { }), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper((Func)(() => new PocoOutput()), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + } + + [Fact] + public async Task HandlerWrapper_HandlerSeesSerializerOnContext() + { + // End-to-end: invoke a handler through the wrapper machinery and confirm + // the user delegate sees context.Serializer == registered serializer. + // + // This validates the LambdaBootstrap → RuntimeApiClient → LambdaContext path + // by directly wiring a LambdaContext that carries the serializer (the same + // shape the bootstrap produces in production). + ILambdaSerializer observed = null; + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (input, ctx) => + { + observed = ctx.Serializer; + return Task.FromResult(new PocoOutput()); + }, + SharedSerializer); + + var inputBytes = SerializeToBytes(new PocoInput { InputInt = 1, InputString = "x" }); + var invocation = new InvocationRequest + { + InputStream = new MemoryStream(inputBytes), + LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables()), handlerWrapper.Serializer) + }; + + await handlerWrapper.Handler(invocation); + + Assert.Same(SharedSerializer, observed); + } + + [Fact] + public void LambdaBootstrap_ConstructedWithHandlerWrapper_PlumbsSerializerToRuntimeApiClient() + { + // The bootstrap copies HandlerWrapper.Serializer onto its internal + // RuntimeApiClient.Serializer; that field is what the per-invocation + // LambdaContext gets. + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (input, ctx) => Task.FromResult(new PocoOutput()), + SharedSerializer); + + using var bootstrap = new LambdaBootstrap(handlerWrapper); + + var runtimeApiClient = Assert.IsType(bootstrap.Client); + Assert.Same(SharedSerializer, runtimeApiClient.Serializer); + } + + [Fact] + public void LambdaBootstrap_ConstructedWithRawHandler_HasNullSerializerOnRuntimeApiClient() + { + // Users who construct LambdaBootstrap directly with a LambdaBootstrapHandler + // bypass HandlerWrapper. There's no serializer to capture, so the field stays null. + using var bootstrap = new LambdaBootstrap(_ => Task.FromResult(new InvocationResponse(new MemoryStream(), false))); + + var runtimeApiClient = Assert.IsType(bootstrap.Client); + Assert.Null(runtimeApiClient.Serializer); + } + + private static byte[] SerializeToBytes(T value) + { + using var ms = new MemoryStream(); + SharedSerializer.Serialize(value, ms); + return ms.ToArray(); + } + } +} From 9c9fdbb4d512303625bdc737b552a8c3963489f6 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 14 May 2026 20:00:49 -0400 Subject: [PATCH 2/5] Address PR review feedback - Doc wording: clarify that the Lambda function (not "the runtime") registers the serializer, and call out both registration paths (LambdaBootstrapBuilder for custom runtimes, [assembly: LambdaSerializer] for class-library mode). - Mark ILambdaContext.Serializer as preview ([Experimental("AWSLAMBDA001")]) since class-library mode requires an updated managed runtime to populate it. - Plumb [assembly: LambdaSerializer] for class-library mode: UserCodeLoader exposes the constructed serializer; RuntimeSupportInitializer pushes it onto LambdaBootstrap inside the wrapped initializer callback. - Defensive forward-compat: replace constructor injection with an internal setter on LambdaContext, and route the assignment through a new LambdaContextSerializerIsolated shim. The Isolated pattern (mirroring TraceProviderIsolated and the SnapStart helpers) defers JIT resolution of the new ILambdaContext.Serializer member to a single one-line method, so a TypeLoadException from a stale Amazon.Lambda.Core in the user's function can be caught at the call site without crashing the invoke loop. After the first failure, _disableSerializerOnContext short-circuits subsequent attempts. --- .../src/Amazon.Lambda.Core/ILambdaContext.cs | 22 +++-- .../Bootstrap/LambdaBootstrap.cs | 60 ++++++++++++- .../Bootstrap/UserCodeLoader.cs | 12 +++ .../Client/RuntimeApiClient.cs | 13 +-- .../Context/LambdaContext.cs | 23 ++--- .../LambdaContextSerializerIsolated.cs | 38 ++++++++ .../RuntimeSupportInitializer.cs | 20 ++++- .../TestLambdaContextSerializerTest.cs | 2 +- .../LambdaContextSerializerTests.cs | 90 ++++++++++--------- 9 files changed, 203 insertions(+), 77 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs diff --git a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs index df0ef848f..21f7a54f6 100644 --- a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs @@ -1,6 +1,7 @@ namespace Amazon.Lambda.Core { using System; + using System.Diagnostics.CodeAnalysis; /// /// Object that allows you to access useful information available within @@ -97,13 +98,22 @@ public interface ILambdaContext string TraceId { get { return string.Empty; } } /// - /// The Lambda serializer registered for the current invocation. When the function - /// is run via Amazon.Lambda.RuntimeSupport, this is the same - /// instance passed to - /// LambdaBootstrapBuilder.Create / HandlerWrapper.GetHandlerWrapper, - /// allowing user code to reuse it for ad-hoc serialization. Can be null - /// when the runtime did not register a serializer (e.g., raw-stream handlers). + /// The the Lambda function registered with the + /// runtime — either the instance passed to + /// LambdaBootstrapBuilder.Create(handler, serializer) / + /// HandlerWrapper.GetHandlerWrapper(handler, serializer), or the type set + /// via [assembly: LambdaSerializer(typeof(...))] in class-library mode. + /// User code can reuse it for ad-hoc (de)serialization without re-instantiating. + /// Can be null when the function did not register a serializer (e.g., raw-stream + /// handlers). /// + /// + /// Preview API. Class-library mode requires an updated managed + /// Lambda runtime to populate this property; until that ships, the value will + /// be null when running in class-library mode. The + /// is applied to surface this caveat at the call site. + /// + [Experimental("AWSLAMBDA001")] ILambdaSerializer Serializer { get { return null; } } #endif } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index 4acfc54cd..3fa1fe87c 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -52,7 +52,16 @@ public class LambdaBootstrap : IDisposable private readonly LambdaBootstrapInitializer _initializer; private readonly LambdaBootstrapHandler _handler; + // Mutable so RuntimeSupportInitializer (class-library mode) can set this after + // UserCodeLoader.Init resolves [assembly: LambdaSerializer]. Read on every + // invocation to populate ILambdaContext.Serializer via the Isolated shim. + private Amazon.Lambda.Core.ILambdaSerializer _serializer; private readonly bool _ownsHttpClient; + // Set true once a TypeLoadException/MissingMethodException has surfaced from the + // Isolated serializer shim, indicating the user's Amazon.Lambda.Core lacks + // ILambdaContext.Serializer. After that point we stop attempting to populate it + // for every subsequent invocation in this process. + private volatile bool _disableSerializerOnContext; private readonly InternalLogger _logger = InternalLogger.GetDefaultLogger(); private readonly HttpClient _httpClient; @@ -62,6 +71,18 @@ public class LambdaBootstrap : IDisposable internal IRuntimeApiClient Client { get; set; } + /// + /// Set the serializer to surface on + /// for each invocation. Used by to plumb the + /// serializer constructed from [assembly: LambdaSerializer] after + /// has initialized. Setter is internal — public + /// callers register the serializer via instead. + /// + internal void SetSerializer(Amazon.Lambda.Core.ILambdaSerializer serializer) + { + _serializer = serializer; + } + /// /// Create a LambdaBootstrap that will call the given initializer and handler. @@ -180,13 +201,12 @@ internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _serializer = serializer; _ownsHttpClient = ownsHttpClient; _initializer = initializer; _httpClient.Timeout = RuntimeApiHttpTimeout; _environmentVariables = environmentVariables ?? new SystemEnvironmentVariables(); - var runtimeApiClient = new RuntimeApiClient(_environmentVariables, _httpClient, lambdaBootstrapOptions); - runtimeApiClient.Serializer = serializer; - Client = runtimeApiClient; + Client = new RuntimeApiClient(_environmentVariables, _httpClient, lambdaBootstrapOptions); _configuration = configuration ?? LambdaBootstrapConfiguration.GetDefaultConfiguration(_environmentVariables); _awsSdkTraceIdSetter = Utils.FindAWSSDKTraceIdSetter(_environmentVariables); @@ -368,6 +388,7 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul { Client.ConsoleLogger.SetRuntimeHeaders(impl.RuntimeApiHeaders); SetInvocationTraceId(impl.RuntimeApiHeaders.TraceId); + SetSerializerOnContext(impl); } // Initialize ResponseStreamFactory — includes RuntimeApiClient reference @@ -484,6 +505,39 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul } } + private void SetSerializerOnContext(LambdaContext context) + { + // No serializer was registered with this bootstrap (raw-stream handler, or + // the user constructed LambdaBootstrap with a LambdaBootstrapHandler directly). + // Nothing to surface — leave context.Serializer null. + if (_serializer == null) return; + + // A previous invocation hit a TypeLoadException / MissingMethodException from + // an older Amazon.Lambda.Core in the user's function. Don't keep trying. + if (_disableSerializerOnContext) return; + + try + { + LambdaContextSerializerIsolated.TrySetSerializer(context, _serializer); + } + catch (MissingMethodException) + { + _disableSerializerOnContext = true; + _logger.LogInformation( + "Failed to set the serializer on ILambdaContext.Serializer due to the version of " + + "Amazon.Lambda.Core referenced by the Lambda function being out of date. The serializer " + + "will not be exposed via ILambdaContext for the remainder of this process."); + } + catch (TypeLoadException) + { + _disableSerializerOnContext = true; + _logger.LogInformation( + "Failed to set the serializer on ILambdaContext.Serializer due to the version of " + + "Amazon.Lambda.Core referenced by the Lambda function being out of date. The serializer " + + "will not be exposed via ILambdaContext for the remainder of this process."); + } + } + volatile bool _disableTraceProvider = false; private void SetInvocationTraceId(string traceId) { diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs index 84b3d7aa4..d14d05c5b 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs @@ -46,6 +46,17 @@ internal class UserCodeLoader private Action _invokeDelegate; internal MethodInfo CustomerMethodInfo { get; private set; } + /// + /// The serializer instance constructed from the customer's + /// [LambdaSerializer(typeof(...))] attribute (if any). Populated by + /// . Typed as here because user code may + /// reference an older Amazon.Lambda.Core than what RuntimeSupport + /// references — the cross-version identity is + /// not guaranteed. The Isolated shim that exposes the value via + /// handles the cross-version cast. + /// + internal object CustomerSerializerInstance { get; private set; } + /// /// Initializes UserCodeLoader with a given handler and internal logger. /// @@ -129,6 +140,7 @@ public void Init(Action customerLoggingAction) var customerObject = GetCustomerObject(customerType); var customerSerializerInstance = GetSerializerObject(customerAssembly); + CustomerSerializerInstance = customerSerializerInstance; _logger.LogDebug($"UCL : Constructing invoke delegate"); var isPreJit = UserCodeInit.IsCallPreJit(_environmentVariables); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs index e71a99485..39cc7d055 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport.Helpers; using System; using System.IO; @@ -38,13 +37,6 @@ public class RuntimeApiClient : IRuntimeApiClient internal Func ExceptionConverter { get; set; } internal LambdaEnvironment LambdaEnvironment { get; set; } - /// - /// Optional serializer attached to the per-invocation - /// in . - /// Set by from the registered . - /// - internal ILambdaSerializer Serializer { get; set; } - /// public IConsoleLoggerWriter ConsoleLogger => _consoleLoggerRedirector; @@ -108,8 +100,7 @@ public Task ReportInitializationErrorAsync(string errorType, CancellationToken c /// /// Get the next function invocation from the Runtime API as an asynchronous operation. - /// Completes when the next invocation is received. The - /// (if set) is attached to the resulting . + /// Completes when the next invocation is received. /// /// The optional cancellation token to use to stop listening for the next invocation. /// A Task representing the asynchronous operation. @@ -118,7 +109,7 @@ public async Task GetNextInvocationAsync(CancellationToken ca SwaggerResponse response = await _internalClient.NextAsync(cancellationToken); var headers = new RuntimeApiHeaders(response.Headers); - var lambdaContext = new LambdaContext(headers, LambdaEnvironment, _consoleLoggerRedirector, Serializer); + var lambdaContext = new LambdaContext(headers, LambdaEnvironment, _consoleLoggerRedirector); return new InvocationRequest { InputStream = response.Result, diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs index 4e8d1dd4f..a3aab9bef 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs @@ -31,31 +31,19 @@ internal class LambdaContext : ILambdaContext private readonly Lazy _cognitoIdentityLazy; private readonly Lazy _cognitoClientContextLazy; private readonly IConsoleLoggerWriter _consoleLogger; - private readonly ILambdaSerializer _serializer; public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IConsoleLoggerWriter consoleLogger) - : this(runtimeApiHeaders, lambdaEnvironment, new DateTimeHelper(), consoleLogger, serializer: null) - { - } - - public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IConsoleLoggerWriter consoleLogger, ILambdaSerializer serializer) - : this(runtimeApiHeaders, lambdaEnvironment, new DateTimeHelper(), consoleLogger, serializer) + : this(runtimeApiHeaders, lambdaEnvironment, new DateTimeHelper(), consoleLogger) { } public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IDateTimeHelper dateTimeHelper, IConsoleLoggerWriter consoleLogger) - : this(runtimeApiHeaders, lambdaEnvironment, dateTimeHelper, consoleLogger, serializer: null) - { - } - - public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IDateTimeHelper dateTimeHelper, IConsoleLoggerWriter consoleLogger, ILambdaSerializer serializer) { _lambdaEnvironment = lambdaEnvironment; _runtimeApiHeaders = runtimeApiHeaders; _dateTimeHelper = dateTimeHelper; _consoleLogger = consoleLogger; - _serializer = serializer; int.TryParse(_lambdaEnvironment.FunctionMemorySize, out _memoryLimitInMB); long.TryParse(_runtimeApiHeaders.DeadlineMs, out _deadlineMs); @@ -89,7 +77,14 @@ public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lamb public string TenantId => _runtimeApiHeaders.TenantId; - public ILambdaSerializer Serializer => _serializer; + /// + /// The serializer the Lambda function registered with the runtime, surfaced via + /// . Set per-invocation by + /// ; the + /// Isolated shim exists so a stale Amazon.Lambda.Core in the user's function + /// (one without ILambdaContext.Serializer) cannot break the invoke loop. + /// + public ILambdaSerializer Serializer { get; internal set; } internal IRuntimeApiHeaders RuntimeApiHeaders => _runtimeApiHeaders; } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs new file mode 100644 index 000000000..90ba0b827 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using Amazon.Lambda.Core; + +namespace Amazon.Lambda.RuntimeSupport.Helpers +{ + /// + /// Wrapper around the call that sets . + /// The user's Lambda function may reference an older + /// that does not declare ILambdaContext.Serializer. By isolating the assignment + /// in its own static method, the JIT only attempts to resolve the property when this + /// method is called — letting the invoke loop wrap it in try/catch for + /// / and + /// continue the invocation rather than crashing the process. + /// + /// Mirrors the pattern used by , + /// , etc. + /// + internal static class LambdaContextSerializerIsolated + { + /// + /// Stores on so + /// returns it on this invocation. + /// + /// Thrown when the user's Amazon.Lambda.Core does + /// not contain . Callers must catch. + /// Thrown by some runtimes when the + /// property accessor is missing on the loaded . + /// Callers must catch. + internal static void TrySetSerializer(LambdaContext context, ILambdaSerializer serializer) + { + if (context == null) return; + context.Serializer = serializer; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs index 02fcfc2c2..0de36f58e 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs @@ -63,11 +63,27 @@ public async Task RunLambdaBootstrap() var environmentVariables = new SystemEnvironmentVariables(); var userCodeLoader = new UserCodeLoader(environmentVariables, _handler, _logger); var initializer = new UserCodeInitializer(userCodeLoader, _logger); + // Pre-declare so the wrapped initializer can reference it. The closure runs + // later (inside bootstrap.RunAsync) by which time bootstrap is assigned. + LambdaBootstrap bootstrap = null; + // Wrap init to plumb the serializer ([assembly: LambdaSerializer]) onto the + // bootstrap right after UserCodeLoader resolves it. The bootstrap then + // surfaces it on ILambdaContext.Serializer for every invocation via the + // Isolated shim. + LambdaBootstrapInitializer wrappedInit = async () => + { + var initResult = await initializer.InitializeAsync(); + if (initResult) + { + bootstrap.SetSerializer(userCodeLoader.CustomerSerializerInstance as Amazon.Lambda.Core.ILambdaSerializer); + } + return initResult; + }; using (var handlerWrapper = HandlerWrapper.GetHandlerWrapper(userCodeLoader.Invoke)) - using (var bootstrap = new LambdaBootstrap( + using (bootstrap = new LambdaBootstrap( httpClient: null, handler: handlerWrapper.Handler, - initializer: initializer.InitializeAsync, + initializer: wrappedInit, ownsHttpClient: true, lambdaBootstrapOptions: _lambdaBootstrapOptions, environmentVariables: environmentVariables)) diff --git a/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs b/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs index 678647394..c806609ee 100644 --- a/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs +++ b/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - +#pragma warning disable AWSLAMBDA001 // ILambdaContext.Serializer is preview; this is the test that proves it works. using System.IO; using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs index 55db8c462..7cda0d6e1 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs @@ -12,7 +12,9 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ +#pragma warning disable AWSLAMBDA001 // ILambdaContext.Serializer is preview; this is the test that proves it works. using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport.Helpers; using Amazon.Lambda.Serialization.Json; using System; using System.Collections.Generic; @@ -31,38 +33,54 @@ public class LambdaContextSerializerTests { private static readonly JsonSerializer SharedSerializer = new JsonSerializer(); + private readonly TestEnvironmentVariables _environmentVariables; private readonly LambdaEnvironment _lambdaEnvironment; private readonly RuntimeApiHeaders _runtimeApiHeaders; + private readonly Dictionary> _headers; public LambdaContextSerializerTests() { - var environmentVariables = new TestEnvironmentVariables(); - _lambdaEnvironment = new LambdaEnvironment(environmentVariables); + _environmentVariables = new TestEnvironmentVariables(); + _lambdaEnvironment = new LambdaEnvironment(_environmentVariables); - var headers = new Dictionary> + _headers = new Dictionary> { [RuntimeApiHeaders.HeaderAwsRequestId] = new[] { "request-id" }, [RuntimeApiHeaders.HeaderInvokedFunctionArn] = new[] { "invoked-function-arn" } }; - _runtimeApiHeaders = new RuntimeApiHeaders(headers); + _runtimeApiHeaders = new RuntimeApiHeaders(_headers); } [Fact] - public void LambdaContext_Serializer_DefaultsToNull_WhenNotSupplied() + public void LambdaContext_Serializer_DefaultsToNull() { - var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())); + var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new LogLevelLoggerWriter(new SystemEnvironmentVariables())); Assert.Null(context.Serializer); } [Fact] - public void LambdaContext_Serializer_ReturnsConstructorArgument() + public void LambdaContextSerializerIsolated_TrySetSerializer_PopulatesProperty() { - var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables()), SharedSerializer); + // The Isolated shim is the one place RuntimeSupport touches + // ILambdaContext.Serializer; everything else routes through this method + // so a TypeLoadException from a stale user-side Amazon.Lambda.Core can be + // caught at the call site. + var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new LogLevelLoggerWriter(new SystemEnvironmentVariables())); + + LambdaContextSerializerIsolated.TrySetSerializer(context, SharedSerializer); Assert.Same(SharedSerializer, context.Serializer); } + [Fact] + public void LambdaContextSerializerIsolated_TrySetSerializer_NullContext_DoesNotThrow() + { + // The shim is called on every invocation; a defensive null-check keeps it + // total even if a future refactor passes a non-LambdaContext implementation. + LambdaContextSerializerIsolated.TrySetSerializer(null, SharedSerializer); + } + [Fact] public void HandlerWrapper_PocoInOut_ExposesSerializer() { @@ -88,7 +106,6 @@ public void HandlerWrapper_AllSerializerOverloads_PropagateSerializer() // One sample per overload family (Func/Action × Task/non-Task × in/out × ILambdaContext) // is enough — they share the same field-assignment line. This guards against future // overloads being added without setting Serializer. - using (var w = HandlerWrapper.GetHandlerWrapper((input) => Task.CompletedTask, SharedSerializer)) Assert.Same(SharedSerializer, w.Serializer); @@ -114,14 +131,12 @@ public void HandlerWrapper_AllSerializerOverloads_PropagateSerializer() } [Fact] - public async Task HandlerWrapper_HandlerSeesSerializerOnContext() + public async Task LambdaBootstrap_InvokeOnce_SetsSerializerOnContext() { - // End-to-end: invoke a handler through the wrapper machinery and confirm - // the user delegate sees context.Serializer == registered serializer. - // - // This validates the LambdaBootstrap → RuntimeApiClient → LambdaContext path - // by directly wiring a LambdaContext that carries the serializer (the same - // shape the bootstrap produces in production). + // End-to-end: a HandlerWrapper-backed bootstrap invokes once against a test + // RuntimeApiClient. The user's handler reads context.Serializer mid-invocation + // and must see the registered instance — proving SetSerializerOnContext fires + // through the Isolated shim during the invoke loop. ILambdaSerializer observed = null; using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( (input, ctx) => @@ -131,43 +146,38 @@ public async Task HandlerWrapper_HandlerSeesSerializerOnContext() }, SharedSerializer); - var inputBytes = SerializeToBytes(new PocoInput { InputInt = 1, InputString = "x" }); - var invocation = new InvocationRequest + using var bootstrap = new LambdaBootstrap(handlerWrapper); + var testClient = new TestRuntimeApiClient(_environmentVariables, _headers) { - InputStream = new MemoryStream(inputBytes), - LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables()), handlerWrapper.Serializer) + FunctionInput = SerializeToBytes(new PocoInput { InputInt = 1, InputString = "x" }) }; + bootstrap.Client = testClient; - await handlerWrapper.Handler(invocation); + await bootstrap.InvokeOnceAsync(); Assert.Same(SharedSerializer, observed); } [Fact] - public void LambdaBootstrap_ConstructedWithHandlerWrapper_PlumbsSerializerToRuntimeApiClient() + public async Task LambdaBootstrap_InvokeOnce_RawStreamHandler_LeavesSerializerNull() { - // The bootstrap copies HandlerWrapper.Serializer onto its internal - // RuntimeApiClient.Serializer; that field is what the per-invocation - // LambdaContext gets. - using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( - (input, ctx) => Task.FromResult(new PocoOutput()), - SharedSerializer); + // Raw-stream handlers don't register a serializer — context.Serializer must + // stay null even after the invoke loop runs. + ILambdaSerializer observed = SharedSerializer; // start non-null to prove it's set to null + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (Func)((input, ctx) => + { + observed = ctx.Serializer; + return Task.CompletedTask; + })); using var bootstrap = new LambdaBootstrap(handlerWrapper); + var testClient = new TestRuntimeApiClient(_environmentVariables, _headers); + bootstrap.Client = testClient; - var runtimeApiClient = Assert.IsType(bootstrap.Client); - Assert.Same(SharedSerializer, runtimeApiClient.Serializer); - } - - [Fact] - public void LambdaBootstrap_ConstructedWithRawHandler_HasNullSerializerOnRuntimeApiClient() - { - // Users who construct LambdaBootstrap directly with a LambdaBootstrapHandler - // bypass HandlerWrapper. There's no serializer to capture, so the field stays null. - using var bootstrap = new LambdaBootstrap(_ => Task.FromResult(new InvocationResponse(new MemoryStream(), false))); + await bootstrap.InvokeOnceAsync(); - var runtimeApiClient = Assert.IsType(bootstrap.Client); - Assert.Null(runtimeApiClient.Serializer); + Assert.Null(observed); } private static byte[] SerializeToBytes(T value) From b6d51590b4688873b0cde32f20a7a8d0738b4946 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Fri, 15 May 2026 10:28:27 -0400 Subject: [PATCH 3/5] Address Copilot PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LambdaContextSerializerIsolated: replace cref pointing at the Amazon.Lambda.Core namespace with plain text so XML doc generation doesn't trip the warnings-as-errors build. - UserCodeLoader.CustomerSerializerInstance: rewrite the doc comment to match the implementation — the cross-version cast happens in RuntimeSupportInitializer, not in the Isolated shim. - TestLambdaContext.Serializer: apply [Experimental("AWSLAMBDA001")] so the test-utility surface mirrors the preview opt-in on ILambdaContext.Serializer. - Tests: rename HandlerWrapper_AllSerializerOverloads_PropagateSerializer to HandlerWrapper_SerializerOverloadFamilies_PropagateSerializer to reflect that it samples one overload per family rather than asserting every overload signature. Add two tests covering the class-library serializer path: UserCodeLoader.Init resolving an [assembly: LambdaSerializer] attribute, and LambdaBootstrap.SetSerializer flowing that instance to ILambdaContext.Serializer through the invoke loop. --- .../Bootstrap/UserCodeLoader.cs | 10 +-- .../LambdaContextSerializerIsolated.cs | 2 +- .../TestLambdaContext.cs | 6 ++ .../LambdaContextSerializerTests.cs | 64 +++++++++++++++++-- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs index d14d05c5b..5c079a7d8 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs @@ -49,11 +49,11 @@ internal class UserCodeLoader /// /// The serializer instance constructed from the customer's /// [LambdaSerializer(typeof(...))] attribute (if any). Populated by - /// . Typed as here because user code may - /// reference an older Amazon.Lambda.Core than what RuntimeSupport - /// references — the cross-version identity is - /// not guaranteed. The Isolated shim that exposes the value via - /// handles the cross-version cast. + /// . Typed as here because the value is + /// produced via reflection in and validated + /// against the loaded ILambdaSerializer interface there; + /// casts it back to ILambdaSerializer + /// before handing it to . /// internal object CustomerSerializerInstance { get; private set; } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs index 90ba0b827..e1b9e0d5c 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs @@ -8,7 +8,7 @@ namespace Amazon.Lambda.RuntimeSupport.Helpers { /// /// Wrapper around the call that sets . - /// The user's Lambda function may reference an older + /// The user's Lambda function may reference an older Amazon.Lambda.Core /// that does not declare ILambdaContext.Serializer. By isolating the assignment /// in its own static method, the JIT only attempts to resolve the property when this /// method is called — letting the invoke loop wrap it in try/catch for diff --git a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs index b472403e0..07723b13c 100644 --- a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -86,6 +87,11 @@ public class TestLambdaContext : ILambdaContext /// to mirror the serializer that the Lambda runtime support library would attach /// in production. /// + /// + /// Preview API. Mirrors the experimental ILambdaContext.Serializer + /// surface so test code opts in via the same AWSLAMBDA001 diagnostic. + /// + [Experimental("AWSLAMBDA001")] public ILambdaSerializer Serializer { get; set; } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs index 7cda0d6e1..16123143e 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs @@ -14,6 +14,7 @@ */ #pragma warning disable AWSLAMBDA001 // ILambdaContext.Serializer is preview; this is the test that proves it works. using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport.Bootstrap; using Amazon.Lambda.RuntimeSupport.Helpers; using Amazon.Lambda.Serialization.Json; using System; @@ -101,11 +102,12 @@ public void HandlerWrapper_RawStreamOverloads_HaveNullSerializer() } [Fact] - public void HandlerWrapper_AllSerializerOverloads_PropagateSerializer() + public void HandlerWrapper_SerializerOverloadFamilies_PropagateSerializer() { - // One sample per overload family (Func/Action × Task/non-Task × in/out × ILambdaContext) - // is enough — they share the same field-assignment line. This guards against future - // overloads being added without setting Serializer. + // One sample per overload family (Func/Action × Task/non-Task × in/out × ILambdaContext) — + // they share the same field-assignment line. This guards against future overloads being + // added without setting Serializer, but only spot-checks each family rather than every + // overload signature. using (var w = HandlerWrapper.GetHandlerWrapper((input) => Task.CompletedTask, SharedSerializer)) Assert.Same(SharedSerializer, w.Serializer); @@ -180,6 +182,60 @@ public async Task LambdaBootstrap_InvokeOnce_RawStreamHandler_LeavesSerializerNu Assert.Null(observed); } + [Fact] + public void UserCodeLoader_Init_PopulatesCustomerSerializerFromAssemblyAttribute() + { + // Class-library mode: [assembly: LambdaSerializer(typeof(JsonSerializer))] on the + // HandlerTest assembly should make UserCodeLoader.Init resolve a JsonSerializer + // instance. This is what RuntimeSupportInitializer reads and pushes onto + // LambdaBootstrap.SetSerializer in production. + var ucl = new UserCodeLoader( + new SystemEnvironmentVariables(), + "HandlerTest::HandlerTest.CustomerType::ZeroInZeroOut", + InternalLogger.NoOpLogger); + + ucl.Init(message => { }); + + Assert.NotNull(ucl.CustomerSerializerInstance); + Assert.IsType(ucl.CustomerSerializerInstance); + } + + [Fact] + public async Task LambdaBootstrap_SetSerializer_FlowsAssemblySerializerToContext() + { + // End-to-end class-library wiring: the value UserCodeLoader.Init resolves from + // [assembly: LambdaSerializer] is what RuntimeSupportInitializer pushes onto the + // bootstrap via SetSerializer, after which the invoke loop must surface it on + // ILambdaContext.Serializer for every invocation. + var ucl = new UserCodeLoader( + new SystemEnvironmentVariables(), + "HandlerTest::HandlerTest.CustomerType::ZeroInZeroOut", + InternalLogger.NoOpLogger); + ucl.Init(message => { }); + var assemblySerializer = (ILambdaSerializer)ucl.CustomerSerializerInstance; + + ILambdaSerializer observed = null; + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (input, ctx) => + { + observed = ctx.Serializer; + return Task.FromResult(new PocoOutput()); + }, + SharedSerializer); + + using var bootstrap = new LambdaBootstrap(handlerWrapper); + bootstrap.SetSerializer(assemblySerializer); + var testClient = new TestRuntimeApiClient(_environmentVariables, _headers) + { + FunctionInput = SerializeToBytes(new PocoInput { InputInt = 1, InputString = "x" }) + }; + bootstrap.Client = testClient; + + await bootstrap.InvokeOnceAsync(); + + Assert.Same(assemblySerializer, observed); + } + private static byte[] SerializeToBytes(T value) { using var ms = new MemoryStream(); From eace8c7c4b2266d50515f7aea7633ec00be923f8 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Fri, 15 May 2026 15:54:15 -0400 Subject: [PATCH 4/5] PR comments --- .autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json | 6 +++--- .../Bootstrap/HandlerWrapper.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json b/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json index 665efc4ca..306989ddb 100644 --- a/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json +++ b/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json @@ -4,21 +4,21 @@ "Name": "Amazon.Lambda.Core", "Type": "Minor", "ChangelogMessages": [ - "Add ILambdaSerializer Serializer property to ILambdaContext (default-implemented to null on net8.0+) so user code can access the serializer registered with the runtime" + "Add preview ILambdaSerializer Serializer property to ILambdaContext (default-implemented to null on net8.0+) so user code can access the serializer registered with the runtime. Marked [Experimental(\"AWSLAMBDA001\")]; class-library mode requires an updated managed Lambda runtime to populate this property. The Experimental flag will be removed in a follow-up release once the managed runtime is deployed." ] }, { "Name": "Amazon.Lambda.RuntimeSupport", "Type": "Minor", "ChangelogMessages": [ - "Expose the registered ILambdaSerializer on HandlerWrapper.Serializer and propagate it to the per-invocation ILambdaContext.Serializer" + "Propagate the registered ILambdaSerializer to the per-invocation ILambdaContext.Serializer. Surfaces the new preview ILambdaContext.Serializer (AWSLAMBDA001); the Experimental flag will be removed in a follow-up release once the managed runtime is deployed." ] }, { "Name": "Amazon.Lambda.TestUtilities", "Type": "Minor", "ChangelogMessages": [ - "Add Serializer setter to TestLambdaContext to mirror the new ILambdaContext.Serializer property" + "Add Serializer setter to TestLambdaContext to mirror the new preview ILambdaContext.Serializer property. Marked [Experimental(\"AWSLAMBDA001\")]; the Experimental flag will be removed in a follow-up release once the managed runtime is deployed." ] } ] diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs index 8add132e9..1981d5509 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs @@ -42,7 +42,7 @@ public class HandlerWrapper : IDisposable /// , allowing user code to reuse it. /// Null for handlers that don't take a typed input/output. /// - public ILambdaSerializer Serializer { get; private set; } + internal ILambdaSerializer Serializer { get; set; } private HandlerWrapper(LambdaBootstrapHandler handler) { From abde65364ec9f2e59827378b69b50cef8dc5b128 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Fri, 15 May 2026 16:30:22 -0400 Subject: [PATCH 5/5] updates to remove shim --- .../Bootstrap/LambdaBootstrap.cs | 32 +--------------- .../Context/LambdaContext.cs | 6 +-- .../LambdaContextSerializerIsolated.cs | 38 ------------------- .../LambdaContextSerializerTests.cs | 24 +----------- 4 files changed, 5 insertions(+), 95 deletions(-) delete mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index 3fa1fe87c..130ddf1d4 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -54,14 +54,9 @@ public class LambdaBootstrap : IDisposable private readonly LambdaBootstrapHandler _handler; // Mutable so RuntimeSupportInitializer (class-library mode) can set this after // UserCodeLoader.Init resolves [assembly: LambdaSerializer]. Read on every - // invocation to populate ILambdaContext.Serializer via the Isolated shim. + // invocation to populate ILambdaContext.Serializer. private Amazon.Lambda.Core.ILambdaSerializer _serializer; private readonly bool _ownsHttpClient; - // Set true once a TypeLoadException/MissingMethodException has surfaced from the - // Isolated serializer shim, indicating the user's Amazon.Lambda.Core lacks - // ILambdaContext.Serializer. After that point we stop attempting to populate it - // for every subsequent invocation in this process. - private volatile bool _disableSerializerOnContext; private readonly InternalLogger _logger = InternalLogger.GetDefaultLogger(); private readonly HttpClient _httpClient; @@ -512,30 +507,7 @@ private void SetSerializerOnContext(LambdaContext context) // Nothing to surface — leave context.Serializer null. if (_serializer == null) return; - // A previous invocation hit a TypeLoadException / MissingMethodException from - // an older Amazon.Lambda.Core in the user's function. Don't keep trying. - if (_disableSerializerOnContext) return; - - try - { - LambdaContextSerializerIsolated.TrySetSerializer(context, _serializer); - } - catch (MissingMethodException) - { - _disableSerializerOnContext = true; - _logger.LogInformation( - "Failed to set the serializer on ILambdaContext.Serializer due to the version of " + - "Amazon.Lambda.Core referenced by the Lambda function being out of date. The serializer " + - "will not be exposed via ILambdaContext for the remainder of this process."); - } - catch (TypeLoadException) - { - _disableSerializerOnContext = true; - _logger.LogInformation( - "Failed to set the serializer on ILambdaContext.Serializer due to the version of " + - "Amazon.Lambda.Core referenced by the Lambda function being out of date. The serializer " + - "will not be exposed via ILambdaContext for the remainder of this process."); - } + context.Serializer = _serializer; } volatile bool _disableTraceProvider = false; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs index a3aab9bef..fea4a6bd2 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs @@ -79,10 +79,8 @@ public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lamb /// /// The serializer the Lambda function registered with the runtime, surfaced via - /// . Set per-invocation by - /// ; the - /// Isolated shim exists so a stale Amazon.Lambda.Core in the user's function - /// (one without ILambdaContext.Serializer) cannot break the invoke loop. + /// . Assigned per-invocation by + /// . /// public ILambdaSerializer Serializer { get; internal set; } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs deleted file mode 100644 index e1b9e0d5c..000000000 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System; -using Amazon.Lambda.Core; - -namespace Amazon.Lambda.RuntimeSupport.Helpers -{ - /// - /// Wrapper around the call that sets . - /// The user's Lambda function may reference an older Amazon.Lambda.Core - /// that does not declare ILambdaContext.Serializer. By isolating the assignment - /// in its own static method, the JIT only attempts to resolve the property when this - /// method is called — letting the invoke loop wrap it in try/catch for - /// / and - /// continue the invocation rather than crashing the process. - /// - /// Mirrors the pattern used by , - /// , etc. - /// - internal static class LambdaContextSerializerIsolated - { - /// - /// Stores on so - /// returns it on this invocation. - /// - /// Thrown when the user's Amazon.Lambda.Core does - /// not contain . Callers must catch. - /// Thrown by some runtimes when the - /// property accessor is missing on the loaded . - /// Callers must catch. - internal static void TrySetSerializer(LambdaContext context, ILambdaSerializer serializer) - { - if (context == null) return; - context.Serializer = serializer; - } - } -} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs index 16123143e..8b62c25ef 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs @@ -60,28 +60,6 @@ public void LambdaContext_Serializer_DefaultsToNull() Assert.Null(context.Serializer); } - [Fact] - public void LambdaContextSerializerIsolated_TrySetSerializer_PopulatesProperty() - { - // The Isolated shim is the one place RuntimeSupport touches - // ILambdaContext.Serializer; everything else routes through this method - // so a TypeLoadException from a stale user-side Amazon.Lambda.Core can be - // caught at the call site. - var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new LogLevelLoggerWriter(new SystemEnvironmentVariables())); - - LambdaContextSerializerIsolated.TrySetSerializer(context, SharedSerializer); - - Assert.Same(SharedSerializer, context.Serializer); - } - - [Fact] - public void LambdaContextSerializerIsolated_TrySetSerializer_NullContext_DoesNotThrow() - { - // The shim is called on every invocation; a defensive null-check keeps it - // total even if a future refactor passes a non-LambdaContext implementation. - LambdaContextSerializerIsolated.TrySetSerializer(null, SharedSerializer); - } - [Fact] public void HandlerWrapper_PocoInOut_ExposesSerializer() { @@ -138,7 +116,7 @@ public async Task LambdaBootstrap_InvokeOnce_SetsSerializerOnContext() // End-to-end: a HandlerWrapper-backed bootstrap invokes once against a test // RuntimeApiClient. The user's handler reads context.Serializer mid-invocation // and must see the registered instance — proving SetSerializerOnContext fires - // through the Isolated shim during the invoke loop. + // during the invoke loop. ILambdaSerializer observed = null; using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( (input, ctx) =>