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..306989ddb --- /dev/null +++ b/.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json @@ -0,0 +1,25 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Core", + "Type": "Minor", + "ChangelogMessages": [ + "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": [ + "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 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.Core/ILambdaContext.cs b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs index 81f290408..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 @@ -95,6 +96,25 @@ public interface ILambdaContext /// The trace id generated by Lambda for distributed tracing across AWS services. /// string TraceId { get { return string.Empty; } } + + /// + /// 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/HandlerWrapper.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs index e3a74d04b..1981d5509 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. + /// + internal ILambdaSerializer Serializer { get; 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..130ddf1d4 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -52,6 +52,10 @@ 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. + private Amazon.Lambda.Core.ILambdaSerializer _serializer; private readonly bool _ownsHttpClient; private readonly InternalLogger _logger = InternalLogger.GetDefaultLogger(); @@ -62,6 +66,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. @@ -101,7 +117,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 +127,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 +138,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 +149,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 +186,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) { @@ -179,6 +196,7 @@ 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; @@ -365,6 +383,7 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul { Client.ConsoleLogger.SetRuntimeHeaders(impl.RuntimeApiHeaders); SetInvocationTraceId(impl.RuntimeApiHeaders.TraceId); + SetSerializerOnContext(impl); } // Initialize ResponseStreamFactory — includes RuntimeApiClient reference @@ -481,6 +500,16 @@ 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; + + context.Serializer = _serializer; + } + 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..5c079a7d8 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 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; } + /// /// 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/Context/LambdaContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs index cca6f4e5d..fea4a6bd2 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs @@ -77,6 +77,13 @@ public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lamb public string TenantId => _runtimeApiHeaders.TenantId; + /// + /// The serializer the Lambda function registered with the runtime, surfaced via + /// . Assigned per-invocation by + /// . + /// + public ILambdaSerializer Serializer { get; internal set; } + internal IRuntimeApiHeaders RuntimeApiHeaders => _runtimeApiHeaders; } } 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/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs index e3a47d308..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; @@ -80,5 +81,17 @@ 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. + /// + /// + /// 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.Core.Tests/TestLambdaContextSerializerTest.cs b/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs new file mode 100644 index 000000000..c806609ee --- /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 +#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; +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..8b62c25ef --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs @@ -0,0 +1,224 @@ +/* + * 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. + */ +#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; +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 TestEnvironmentVariables _environmentVariables; + private readonly LambdaEnvironment _lambdaEnvironment; + private readonly RuntimeApiHeaders _runtimeApiHeaders; + private readonly Dictionary> _headers; + + public LambdaContextSerializerTests() + { + _environmentVariables = new TestEnvironmentVariables(); + _lambdaEnvironment = new LambdaEnvironment(_environmentVariables); + + _headers = new Dictionary> + { + [RuntimeApiHeaders.HeaderAwsRequestId] = new[] { "request-id" }, + [RuntimeApiHeaders.HeaderInvokedFunctionArn] = new[] { "invoked-function-arn" } + }; + _runtimeApiHeaders = new RuntimeApiHeaders(_headers); + } + + [Fact] + public void LambdaContext_Serializer_DefaultsToNull() + { + var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new LogLevelLoggerWriter(new SystemEnvironmentVariables())); + + Assert.Null(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_SerializerOverloadFamilies_PropagateSerializer() + { + // 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); + + 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 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 + // during the invoke loop. + 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); + var testClient = new TestRuntimeApiClient(_environmentVariables, _headers) + { + FunctionInput = SerializeToBytes(new PocoInput { InputInt = 1, InputString = "x" }) + }; + bootstrap.Client = testClient; + + await bootstrap.InvokeOnceAsync(); + + Assert.Same(SharedSerializer, observed); + } + + [Fact] + public async Task LambdaBootstrap_InvokeOnce_RawStreamHandler_LeavesSerializerNull() + { + // 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; + + await bootstrap.InvokeOnceAsync(); + + 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(); + SharedSerializer.Serialize(value, ms); + return ms.ToArray(); + } + } +}