diff --git a/.autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json b/.autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json new file mode 100644 index 000000000..4a85a6c5a --- /dev/null +++ b/.autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Patch", + "ChangelogMessages": [ + "Fixed ReflectionTypeLoadException when customer bundles Amazon.Lambda.Core \u003C 3.0.0 and runtime scans types. Replaced direct ILambdaResponseStream implementation with DispatchProxy to avoid exposing the type in assembly metadata." + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs index 2cb46e3ce..fff3d06e5 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs @@ -2,58 +2,109 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Amazon.Lambda.Core.ResponseStreaming; using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; #pragma warning disable CA2252 namespace Amazon.Lambda.RuntimeSupport { /// - /// This class is used to connect the created by to Amazon.Lambda.Core with it's public interfaces. - /// The deployed Lambda function might be referencing an older version of Amazon.Lambda.Core that does not have the public interfaces for response streaming, - /// so this class is used to avoid a direct dependency on Amazon.Lambda.Core in the rest of the response streaming implementation. + /// This class connects the created by + /// to Amazon.Lambda.Core's public response streaming interfaces. /// - /// Any code referencing this class must wrap the code around a try/catch for to allow for the case where the Lambda function - /// is deployed with an older version of Amazon.Lambda.Core that does not have the response streaming interfaces. + /// The deployed Lambda function might reference an older Amazon.Lambda.Core that does not have + /// the response streaming interfaces. To prevent ReflectionTypeLoadException when customer code + /// calls GetTypes() on this assembly, NO type in this class (or this assembly) directly implements + /// ILambdaResponseStream. Instead, we use DispatchProxy to generate the implementation dynamically + /// at runtime, only when the interface type is confirmed to exist in the loaded Amazon.Lambda.Core. + /// See: https://github.com/aws/aws-lambda-dotnet/issues/2430 /// /// internal class ResponseStreamLambdaCoreInitializerIsolated { /// - /// Initalize Amazon.Lambda.Core with a factory method for creating that wraps the internal implementation. + /// Initialize Amazon.Lambda.Core with a factory method for creating response streams. + /// All type references to ILambdaResponseStream are made via reflection to avoid embedding + /// the type dependency in this assembly's metadata. /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Response streaming types are preserved by the runtime and only loaded when available")] + [UnconditionalSuppressMessage("Trimming", "IL2055", Justification = "Response streaming types are preserved by the runtime")] + [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "Response streaming types are preserved by the runtime")] + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Response streaming types are preserved by the runtime")] + [UnconditionalSuppressMessage("Trimming", "IL2080", Justification = "Response streaming types are preserved by the runtime")] internal static void InitializeCore() { -#if !ANALYZER_UNIT_TESTS // This precompiler directive is used to avoid the unit tests from needing a dependency on Amazon.Lambda.Core. - Func factory = (byte[] prelude) => new ImplLambdaResponseStream(ResponseStreamFactory.CreateStream(prelude)); - LambdaResponseStreamFactory.SetLambdaResponseStream(factory); +#if !ANALYZER_UNIT_TESTS + var coreAssembly = typeof(Amazon.Lambda.Core.ILambdaContext).Assembly; + + // Check if the loaded Amazon.Lambda.Core has the response streaming types. + // If not (older version loaded from /var/task), bail out gracefully — no exception. + var interfaceType = coreAssembly.GetType("Amazon.Lambda.Core.ResponseStreaming.ILambdaResponseStream"); + if (interfaceType == null) return; + + var factoryType = coreAssembly.GetType("Amazon.Lambda.Core.ResponseStreaming.LambdaResponseStreamFactory"); + if (factoryType == null) return; + + var setMethod = factoryType.GetMethod("SetLambdaResponseStream", BindingFlags.NonPublic | BindingFlags.Static); + if (setMethod == null) return; + + // Create a Func using DispatchProxy. + // ResponseStreamProxy generates a runtime type implementing T (ILambdaResponseStream) + // that forwards calls to a ResponseStream instance. + var proxyFactoryMethod = typeof(ResponseStreamLambdaCoreInitializerIsolated) + .GetMethod(nameof(BuildFactory), BindingFlags.NonPublic | BindingFlags.Static) + .MakeGenericMethod(interfaceType); + + var factory = proxyFactoryMethod.Invoke(null, null); + setMethod.Invoke(null, new object[] { factory }); #endif } - /// - /// Implements the interface by wrapping a . This is used to connect the internal response streaming implementation to the public interfaces in Amazon.Lambda.Core. - /// - internal class ImplLambdaResponseStream : ILambdaResponseStream + private static Func BuildFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : class { - private readonly ResponseStream _innerStream; - - internal ImplLambdaResponseStream(ResponseStream innerStream) + return (byte[] prelude) => { - _innerStream = innerStream; - } + var stream = ResponseStreamFactory.CreateStream(prelude); + var proxy = DispatchProxy.Create(); + ((ResponseStreamProxy)(object)proxy).SetInner(stream); + return proxy; + }; + } + } - /// - public long BytesWritten => _innerStream.BytesWritten; + /// + /// A DispatchProxy that forwards ILambdaResponseStream calls to a ResponseStream instance. + /// This class does NOT implement ILambdaResponseStream at compile time — DispatchProxy generates + /// the interface implementation dynamically at runtime, avoiding the ReflectionTypeLoadException. + /// + internal class ResponseStreamProxy : DispatchProxy + { + private ResponseStream _inner; - /// - public bool HasError => _innerStream.HasError; + internal void SetInner(ResponseStream inner) + { + _inner = inner; + } - /// - public void Dispose() => _innerStream.Dispose(); + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + return targetMethod.Name switch + { + "WriteAsync" => _inner.WriteAsync((byte[])args[0], (int)args[1], (int)args[2], + args.Length > 3 ? (CancellationToken)args[3] : default), + "Dispose" => DoDispose(), + "get_BytesWritten" => _inner.BytesWritten, + "get_HasError" => _inner.HasError, + _ => null + }; + } - /// - public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) => _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + private object DoDispose() + { + _inner.Dispose(); + return null; } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs index 5da3d5e8b..3908020f9 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs @@ -1,3 +1,4 @@ +using Amazon.Lambda.RuntimeSupport.UnitTests; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 #pragma warning disable CA2252 @@ -167,7 +168,7 @@ public class LambdaResponseStreamTests var output = new MemoryStream(); await inner.SetHttpOutputStreamAsync(output); - var implStream = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var implStream = new TestImplLambdaResponseStream(inner); var lambdaStream = new LambdaResponseStream(implStream); return (lambdaStream, output); } @@ -176,7 +177,7 @@ public class LambdaResponseStreamTests public void LambdaResponseStream_IsStreamSubclass() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.IsAssignableFrom(stream); @@ -186,7 +187,7 @@ public void LambdaResponseStream_IsStreamSubclass() public void CanWrite_IsTrue() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.True(stream.CanWrite); @@ -196,7 +197,7 @@ public void CanWrite_IsTrue() public void CanRead_IsFalse() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.False(stream.CanRead); @@ -206,7 +207,7 @@ public void CanRead_IsFalse() public void CanSeek_IsFalse() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.False(stream.CanSeek); @@ -216,7 +217,7 @@ public void CanSeek_IsFalse() public void Read_ThrowsNotImplementedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.Read(new byte[1], 0, 1)); @@ -226,7 +227,7 @@ public void Read_ThrowsNotImplementedException() public void ReadAsync_ThrowsNotImplementedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); // ReadAsync throws synchronously (not async) — capture the thrown task @@ -239,7 +240,7 @@ public void ReadAsync_ThrowsNotImplementedException() public void Seek_ThrowsNotImplementedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); @@ -249,7 +250,7 @@ public void Seek_ThrowsNotImplementedException() public void Position_Get_ThrowsNotSupportedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => _ = stream.Position); @@ -259,7 +260,7 @@ public void Position_Get_ThrowsNotSupportedException() public void Position_Set_ThrowsNotSupportedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.Position = 0); @@ -269,7 +270,7 @@ public void Position_Set_ThrowsNotSupportedException() public void SetLength_ThrowsNotSupportedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.SetLength(100)); @@ -342,7 +343,7 @@ public async Task WriteAsync_DelegatesToInnerResponseStream() var output = new MemoryStream(); await inner.SetHttpOutputStreamAsync(output); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var data = new byte[] { 1, 2, 3 }; await impl.WriteAsync(data, 0, data.Length); @@ -357,7 +358,7 @@ public async Task BytesWritten_ReflectsInnerStreamBytesWritten() var output = new MemoryStream(); await inner.SetHttpOutputStreamAsync(output); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); await impl.WriteAsync(new byte[7], 0, 7); Assert.Equal(7, impl.BytesWritten); @@ -367,7 +368,7 @@ public async Task BytesWritten_ReflectsInnerStreamBytesWritten() public void HasError_InitiallyFalse() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); Assert.False(impl.HasError); } @@ -378,7 +379,7 @@ public void HasError_TrueAfterReportError() var inner = new ResponseStream(Array.Empty()); inner.ReportError(new Exception("test")); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); Assert.True(impl.HasError); } @@ -387,7 +388,7 @@ public void HasError_TrueAfterReportError() public void Dispose_DisposesInnerStream() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); // Should not throw impl.Dispose(); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestImplLambdaResponseStream.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestImplLambdaResponseStream.cs new file mode 100644 index 000000000..00c736b70 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestImplLambdaResponseStream.cs @@ -0,0 +1,26 @@ +// Test-only implementation of ILambdaResponseStream for unit tests. +// In production, DispatchProxy is used instead to avoid the type reference in assembly metadata. +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.Core.ResponseStreaming; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + internal class TestImplLambdaResponseStream : ILambdaResponseStream + { + private readonly ResponseStream _innerStream; + + internal TestImplLambdaResponseStream(ResponseStream innerStream) + { + _innerStream = innerStream; + } + + public long BytesWritten => _innerStream.BytesWritten; + public bool HasError => _innerStream.HasError; + public void Dispose() => _innerStream.Dispose(); + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + => _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } +}