Skip to content

Commit 94b72e2

Browse files
committed
feat(aot): implement Source Generator architecture foundation for Native AOT compatibility
- Add conditional compilation strategy for multi-framework support - Implement IAwsAccessor interface for type-safe AWS SDK private member access - Create AwsAccessorRegistry for thread-safe accessor lookup in .NET 8+ builds - Add SessionReflectionModern using UnsafeAccessor pattern (zero reflection) - Preserve SessionReflectionLegacy for .NET Framework/Standard 2.0 compatibility - Refactor SessionReflection as platform-specific facade with conditional compilation - Enable AOT analyzers (EnableTrimAnalyzer, EnableSingleFileAnalyzer, EnableAotAnalyzer) for .NET 8+ targets - Add proof-of-concept AmazonS3ClientAccessor demonstrating generated accessor pattern - Configure projects for IsAotCompatible=true to enforce IL warning detection Architecture supports: - Legacy frameworks: netstandard2.0, net472 (traditional reflection) - Modern frameworks: net8.0, net9.0 (UnsafeAccessor + Source Generator) - Zero reflection API usage in AOT builds - Fail-fast behavior when AWS SDK internal contracts change - Automatic accessor registration via ModuleInitializer pattern Ready for Source Generator implementation phase. BREAKING CHANGE: SessionReflection implementation now varies by target framework. Type-based overloads will be deprecated in future versions for .NET 8+ targets.
1 parent 53a83b0 commit 94b72e2

File tree

11 files changed

+398
-83
lines changed

11 files changed

+398
-83
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#if NET8_0_OR_GREATER
2+
using System.Runtime.CompilerServices;
3+
using Amazon.S3;
4+
5+
namespace LocalStack.Client.Generated;
6+
7+
/// <summary>
8+
/// Generated accessor for Amazon S3 Client using UnsafeAccessor pattern.
9+
/// This is a proof-of-concept implementation that would be generated by the Source Generator.
10+
/// </summary>
11+
[System.Diagnostics.CodeAnalysis.DynamicDependency("serviceMetadata", typeof(AmazonS3Client))]
12+
internal sealed class AmazonS3ClientAccessor : IAwsAccessor
13+
{
14+
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "serviceMetadata")]
15+
private static extern ref IServiceMetadata GetServiceMetadataField(AmazonS3Client? instance);
16+
17+
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
18+
private static extern AmazonS3Config CreateS3Config();
19+
20+
// Note: We may need to access private fields in AmazonS3Config for region setting
21+
// This depends on AWS SDK internal structure - will be discovered by the generator
22+
public IServiceMetadata GetServiceMetadata()
23+
{
24+
return GetServiceMetadataField(null);
25+
}
26+
27+
public ClientConfig CreateClientConfig()
28+
{
29+
return CreateS3Config();
30+
}
31+
32+
public void SetRegion(ClientConfig clientConfig, RegionEndpoint regionEndpoint)
33+
{
34+
if (clientConfig is AmazonS3Config s3Config)
35+
{
36+
// Use public API if available, otherwise UnsafeAccessor would be generated
37+
s3Config.RegionEndpoint = regionEndpoint;
38+
}
39+
else
40+
{
41+
throw new ArgumentException($"Expected AmazonS3Config, got {clientConfig.GetType().Name}", nameof(clientConfig));
42+
}
43+
}
44+
45+
public bool TrySetForcePathStyle(ClientConfig clientConfig, bool value)
46+
{
47+
if (clientConfig is AmazonS3Config s3Config)
48+
{
49+
s3Config.ForcePathStyle = value;
50+
return true;
51+
}
52+
53+
return false;
54+
}
55+
}
56+
57+
/// <summary>
58+
/// Module initializer to register the S3 accessor.
59+
/// This would be generated automatically by the Source Generator.
60+
/// </summary>
61+
internal static class S3AccessorInitializer
62+
{
63+
[ModuleInitializer]
64+
internal static void RegisterS3Accessor()
65+
{
66+
AwsAccessorRegistry.Register<AmazonS3Client>(new AmazonS3ClientAccessor());
67+
}
68+
}
69+
#endif

src/LocalStack.Client/LocalStack.Client.csproj

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
2626
</PropertyGroup>
2727

28+
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net9.0'">
29+
<IsAotCompatible>true</IsAotCompatible>
30+
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
31+
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
32+
<EnableAotAnalyzer>true</EnableAotAnalyzer>
33+
</PropertyGroup>
34+
2835
<ItemGroup>
2936
<PackageReference Include="AWSSDK.Core" />
3037
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
@@ -51,8 +58,4 @@
5158
<None Include="../../assets/localstack-dotnet-square.png" Pack="true" PackagePath="" />
5259
</ItemGroup>
5360

54-
<PropertyGroup Condition="'$(TargetFramework)' == 'net472'">
55-
<DefineConstants>NET472</DefineConstants>
56-
</PropertyGroup>
57-
5861
</Project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Collections.Concurrent;
2+
3+
namespace LocalStack.Client.Utils;
4+
5+
/// <summary>
6+
/// Thread-safe registry for AWS client accessors.
7+
/// Populated by generated ModuleInitializer methods for .NET 8+ builds.
8+
/// </summary>
9+
public static class AwsAccessorRegistry
10+
{
11+
private static readonly ConcurrentDictionary<Type, IAwsAccessor> _accessors = new();
12+
13+
/// <summary>
14+
/// Registers an accessor for a specific AWS client type.
15+
/// Called by generated ModuleInitializer methods.
16+
/// </summary>
17+
public static void Register<TClient>(IAwsAccessor accessor) where TClient : AmazonServiceClient
18+
{
19+
_accessors.TryAdd(typeof(TClient), accessor);
20+
}
21+
22+
/// <summary>
23+
/// Gets the registered accessor for the specified AWS client type.
24+
/// Throws NotSupportedException if no accessor is registered.
25+
/// </summary>
26+
public static IAwsAccessor Get(Type clientType)
27+
{
28+
if (clientType == null)
29+
{
30+
throw new ArgumentNullException(nameof(clientType));
31+
}
32+
33+
if (_accessors.TryGetValue(clientType, out var accessor))
34+
{
35+
return accessor;
36+
}
37+
38+
throw new NotSupportedException(
39+
$"No AWS accessor registered for client type '{clientType.FullName}'. " +
40+
"Ensure the AWS SDK package is referenced and the project targets .NET 8 or later for AOT compatibility.");
41+
}
42+
43+
/// <summary>
44+
/// Attempts to get the registered accessor for the specified AWS client type.
45+
/// Returns true if found, false otherwise.
46+
/// </summary>
47+
public static bool TryGet(Type clientType, out IAwsAccessor? accessor)
48+
{
49+
return _accessors.TryGetValue(clientType, out accessor);
50+
}
51+
52+
/// <summary>
53+
/// Gets the number of registered accessors.
54+
/// Used for diagnostics and testing.
55+
/// </summary>
56+
public static int Count => _accessors.Count;
57+
58+
/// <summary>
59+
/// Gets all registered client types.
60+
/// Used for diagnostics and testing.
61+
/// </summary>
62+
public static IEnumerable<Type> RegisteredClientTypes => _accessors.Keys;
63+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace LocalStack.Client.Utils;
2+
3+
/// <summary>
4+
/// Interface for type-safe AWS SDK private member access.
5+
/// Implementations are generated at compile-time for .NET 8+ or use reflection for legacy frameworks.
6+
/// </summary>
7+
public interface IAwsAccessor
8+
{
9+
/// <summary>
10+
/// Gets the service metadata for the AWS client.
11+
/// Accesses the private static 'serviceMetadata' field.
12+
/// </summary>
13+
IServiceMetadata GetServiceMetadata();
14+
15+
/// <summary>
16+
/// Creates a new ClientConfig instance for the AWS client.
17+
/// Uses the appropriate constructor for the client's configuration type.
18+
/// </summary>
19+
ClientConfig CreateClientConfig();
20+
21+
/// <summary>
22+
/// Sets the region endpoint on the client configuration.
23+
/// Accesses private fields/properties for region configuration.
24+
/// </summary>
25+
void SetRegion(ClientConfig clientConfig, RegionEndpoint regionEndpoint);
26+
27+
/// <summary>
28+
/// Attempts to set the ForcePathStyle property on the client configuration.
29+
/// Returns true if the property exists and was set, false otherwise.
30+
/// </summary>
31+
bool TrySetForcePathStyle(ClientConfig clientConfig, bool value);
32+
}
Lines changed: 18 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,45 @@
1-
#pragma warning disable S3011 // We need to use reflection to access private fields for service metadata
2-
namespace LocalStack.Client.Utils;
1+
namespace LocalStack.Client.Utils;
32

3+
/// <summary>
4+
/// Platform-specific SessionReflection facade that chooses the appropriate implementation
5+
/// based on the target framework. Uses modern UnsafeAccessor pattern for .NET 8+
6+
/// and traditional reflection for legacy frameworks.
7+
/// </summary>
48
public class SessionReflection : ISessionReflection
59
{
10+
#if NET8_0_OR_GREATER
11+
private static readonly ISessionReflection _implementation = new SessionReflectionModern();
12+
#else
13+
private static readonly ISessionReflection _implementation = new SessionReflectionLegacy();
14+
#endif
15+
616
public IServiceMetadata ExtractServiceMetadata<TClient>() where TClient : AmazonServiceClient
717
{
8-
Type clientType = typeof(TClient);
9-
10-
return ExtractServiceMetadata(clientType);
18+
return _implementation.ExtractServiceMetadata<TClient>();
1119
}
1220

1321
public IServiceMetadata ExtractServiceMetadata(Type clientType)
1422
{
15-
if (clientType == null)
16-
{
17-
throw new ArgumentNullException(nameof(clientType));
18-
}
19-
20-
FieldInfo serviceMetadataField = clientType.GetField("serviceMetadata", BindingFlags.Static | BindingFlags.NonPublic) ??
21-
throw new InvalidOperationException($"Invalid service type {clientType}");
22-
23-
#pragma warning disable CS8600,CS8603 // Not possible to get null value from this private field
24-
var serviceMetadata = (IServiceMetadata)serviceMetadataField.GetValue(null);
25-
26-
return serviceMetadata;
23+
return _implementation.ExtractServiceMetadata(clientType);
2724
}
2825

2926
public ClientConfig CreateClientConfig<TClient>() where TClient : AmazonServiceClient
3027
{
31-
Type clientType = typeof(TClient);
32-
33-
return CreateClientConfig(clientType);
28+
return _implementation.CreateClientConfig<TClient>();
3429
}
3530

3631
public ClientConfig CreateClientConfig(Type clientType)
3732
{
38-
if (clientType == null)
39-
{
40-
throw new ArgumentNullException(nameof(clientType));
41-
}
42-
43-
ConstructorInfo clientConstructorInfo = FindConstructorWithCredentialsAndClientConfig(clientType);
44-
ParameterInfo clientConfigParam = clientConstructorInfo.GetParameters()[1];
45-
46-
return (ClientConfig)Activator.CreateInstance(clientConfigParam.ParameterType);
33+
return _implementation.CreateClientConfig(clientType);
4734
}
4835

4936
public void SetClientRegion(AmazonServiceClient amazonServiceClient, string systemName)
5037
{
51-
if (amazonServiceClient == null)
52-
{
53-
throw new ArgumentNullException(nameof(amazonServiceClient));
54-
}
55-
56-
PropertyInfo? regionEndpointProperty = amazonServiceClient.Config.GetType()
57-
.GetProperty(nameof(amazonServiceClient.Config.RegionEndpoint),
58-
BindingFlags.Public | BindingFlags.Instance);
59-
regionEndpointProperty?.SetValue(amazonServiceClient.Config, RegionEndpoint.GetBySystemName(systemName));
38+
_implementation.SetClientRegion(amazonServiceClient, systemName);
6039
}
6140

6241
public bool SetForcePathStyle(ClientConfig clientConfig, bool value = true)
6342
{
64-
if (clientConfig == null)
65-
{
66-
throw new ArgumentNullException(nameof(clientConfig));
67-
}
68-
69-
PropertyInfo? forcePathStyleProperty = clientConfig.GetType().GetProperty("ForcePathStyle", BindingFlags.Public | BindingFlags.Instance);
70-
71-
if (forcePathStyleProperty == null)
72-
{
73-
return false;
74-
}
75-
76-
forcePathStyleProperty.SetValue(clientConfig, value);
77-
78-
return true;
79-
}
80-
81-
private static ConstructorInfo FindConstructorWithCredentialsAndClientConfig(Type clientType)
82-
{
83-
return clientType.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
84-
.Single(info =>
85-
{
86-
ParameterInfo[] parameterInfos = info.GetParameters();
87-
88-
if (parameterInfos.Length != 2)
89-
{
90-
return false;
91-
}
92-
93-
ParameterInfo credentialsParameter = parameterInfos[0];
94-
ParameterInfo clientConfigParameter = parameterInfos[1];
95-
96-
return credentialsParameter.Name == "credentials" &&
97-
credentialsParameter.ParameterType == typeof(AWSCredentials) &&
98-
clientConfigParameter.Name == "clientConfig" &&
99-
clientConfigParameter.ParameterType.IsSubclassOf(typeof(ClientConfig));
100-
});
43+
return _implementation.SetForcePathStyle(clientConfig, value);
10144
}
10245
}

0 commit comments

Comments
 (0)