Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json
Original file line number Diff line number Diff line change
@@ -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."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// This class is used to connect the <see cref="ResponseStream"/> created by <see cref="ResponseStreamFactory"/> 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 <see cref="ResponseStream"/> created by <see cref="ResponseStreamFactory"/>
/// to Amazon.Lambda.Core's public response streaming interfaces.
/// <para>
/// Any code referencing this class must wrap the code around a try/catch for <see cref="TypeLoadException"/> 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
/// </para>
/// </summary>
internal class ResponseStreamLambdaCoreInitializerIsolated
{
/// <summary>
/// Initalize Amazon.Lambda.Core with a factory method for creating <see cref="ILambdaResponseStream"/> that wraps the internal <see cref="ResponseStream"/> 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.
/// </summary>
[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<byte[], ILambdaResponseStream> 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<byte[], ILambdaResponseStream> using DispatchProxy.
// ResponseStreamProxy<T> 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
}

/// <summary>
/// Implements the <see cref="ILambdaResponseStream"/> interface by wrapping a <see cref="ResponseStream"/>. This is used to connect the internal response streaming implementation to the public interfaces in Amazon.Lambda.Core.
/// </summary>
internal class ImplLambdaResponseStream : ILambdaResponseStream
private static Func<byte[], T> 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<T, ResponseStreamProxy>();
((ResponseStreamProxy)(object)proxy).SetInner(stream);
return proxy;
};
}
}

/// <inheritdoc/>
public long BytesWritten => _innerStream.BytesWritten;
/// <summary>
/// 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.
/// </summary>
internal class ResponseStreamProxy : DispatchProxy
{
private ResponseStream _inner;

/// <inheritdoc/>
public bool HasError => _innerStream.HasError;
internal void SetInner(ResponseStream inner)
{
_inner = inner;
}

/// <inheritdoc/>
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
};
}

/// <inheritdoc/>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -176,7 +177,7 @@ public class LambdaResponseStreamTests
public void LambdaResponseStream_IsStreamSubclass()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.IsAssignableFrom<Stream>(stream);
Expand All @@ -186,7 +187,7 @@ public void LambdaResponseStream_IsStreamSubclass()
public void CanWrite_IsTrue()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.True(stream.CanWrite);
Expand All @@ -196,7 +197,7 @@ public void CanWrite_IsTrue()
public void CanRead_IsFalse()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.False(stream.CanRead);
Expand All @@ -206,7 +207,7 @@ public void CanRead_IsFalse()
public void CanSeek_IsFalse()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.False(stream.CanSeek);
Expand All @@ -216,7 +217,7 @@ public void CanSeek_IsFalse()
public void Read_ThrowsNotImplementedException()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.Throws<NotImplementedException>(() => stream.Read(new byte[1], 0, 1));
Expand All @@ -226,7 +227,7 @@ public void Read_ThrowsNotImplementedException()
public void ReadAsync_ThrowsNotImplementedException()
{
var inner = new ResponseStream(Array.Empty<byte>());
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
Expand All @@ -239,7 +240,7 @@ public void ReadAsync_ThrowsNotImplementedException()
public void Seek_ThrowsNotImplementedException()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.Throws<NotImplementedException>(() => stream.Seek(0, SeekOrigin.Begin));
Expand All @@ -249,7 +250,7 @@ public void Seek_ThrowsNotImplementedException()
public void Position_Get_ThrowsNotSupportedException()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.Throws<NotSupportedException>(() => _ = stream.Position);
Expand All @@ -259,7 +260,7 @@ public void Position_Get_ThrowsNotSupportedException()
public void Position_Set_ThrowsNotSupportedException()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.Throws<NotSupportedException>(() => stream.Position = 0);
Expand All @@ -269,7 +270,7 @@ public void Position_Set_ThrowsNotSupportedException()
public void SetLength_ThrowsNotSupportedException()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);
var stream = new LambdaResponseStream(impl);

Assert.Throws<NotSupportedException>(() => stream.SetLength(100));
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -367,7 +368,7 @@ public async Task BytesWritten_ReflectsInnerStreamBytesWritten()
public void HasError_InitiallyFalse()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);

Assert.False(impl.HasError);
}
Expand All @@ -378,7 +379,7 @@ public void HasError_TrueAfterReportError()
var inner = new ResponseStream(Array.Empty<byte>());
inner.ReportError(new Exception("test"));

var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);

Assert.True(impl.HasError);
}
Expand All @@ -387,7 +388,7 @@ public void HasError_TrueAfterReportError()
public void Dispose_DisposesInnerStream()
{
var inner = new ResponseStream(Array.Empty<byte>());
var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner);
var impl = new TestImplLambdaResponseStream(inner);

// Should not throw
impl.Dispose();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}