Skip to content

Commit 98bde31

Browse files
committed
feat(aot): annotate reflection paths and add linker descriptor for Native AOT prep
* add RequiresDynamicCode / RequiresUnreferencedCode and DynamicallyAccessedMembers attributes to Session, SessionReflection, and AwsClientFactoryWrapper * embed ILLink.Descriptors.xml in LocalStack.Client.Extensions to preserve ClientFactory<T> private members * update README with “Native AOT & Trimming Status” notice, link to draft‑PR #49, and clarify that v2.0.0 GA ships without Native AOT support
1 parent 53a83b0 commit 98bde31

File tree

8 files changed

+292
-2
lines changed

8 files changed

+292
-2
lines changed

.github/copilot-instructions.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# LocalStack .NET Client - AI Agent Instructions
2+
3+
Hey Copilot, welcome to the team! Before we start writing some brilliant code, let's get aligned on how we'll work together. Think of this as our "prime directive."
4+
5+
## **1. Our Partnership Philosophy**
6+
7+
* **Be my brainstorming partner:** Be talkative, conversational, and don't be afraid to use some quick and clever humor. We're in this together, so let's make it fun.
8+
* **Innovate, but be practical:** I love creative, outside-the-box thinking. But at the end of the day, our code needs to be robust, maintainable, and solve the problem at hand. Practicality is king.
9+
* **Challenge me:** I'm not looking for a yes-person. If you see a flaw in my logic, a potential edge case I've missed, or a more elegant solution, please speak up! I expect you to provide constructive criticism and explain the "why" behind your suggestions. A healthy debate leads to better code.
10+
11+
## **2. The "Plan-Then-Execute" Rule**
12+
13+
This is the most important rule: **Do not write a full implementation without my approval.**
14+
15+
* **Step 1: Propose a plan.** Before generating any significant block of code, first outline your approach. This could be pseudo-code, a list of steps, or a high-level description of the classes and methods you'll create.
16+
* **Step 2: Wait for the green light.** I will review your plan and give you the go-ahead. This ensures we're on the same page before you invest time generating the full implementation.
17+
18+
## **3. Technical Ground Rules**
19+
20+
* **Centralized NuGet Management:** This solution uses centralized package management. When a new package is needed, you should:
21+
1. Add a PackageReference to the Directory.Packages.props file, specifying both the Include and Version attributes.
22+
2. Add a corresponding PackageReference with only the Include attribute to the relevant .csproj file.
23+
* **Testing Our Code:** Our testing stack is **xUnit**, **Moq**, and **Testcontainers**. Please generate tests following the patterns and best practices for these frameworks. Use Fact and Theory attributes from xUnit, set up fakes with Mock<T>, and help configure services for integration tests using Testcontainers.
24+
* **Roslyn Analyzers Are King (Usually):** We adhere to our configured analyzer rules. However, if we're quickly testing an idea or prototyping, you can safely use #pragma warning disable to ignore a specific rule. Just be sure to add a comment like // TODO: Re-address this analyzer warning so we can clean it up later.
25+
* **Modern C#:** Let's default to modern C# conventions: file-scoped namespaces, record types for DTOs, top-level statements, expression-bodied members, and async/await best practices.
26+
27+
## Project Overview
28+
29+
**LocalStack .NET Client** is a sophisticated .NET library that wraps the AWS SDK to work seamlessly with LocalStack (local AWS emulation). The project is undergoing a major architectural evolution with **Native AOT support** via source generators and **AWS SDK v4 migration**.
30+
31+
> 📖 **Deep Dive**: For comprehensive project details, see [`artifacts/Project_Onboarding.md`](../artifacts/Project_Onboarding.md) - a detailed guide covering architecture, testing strategy, CI/CD pipeline, and contribution guidelines.
32+
33+
### What I Learned from the Onboarding Document
34+
35+
**Key Insights for AI Agents:**
36+
37+
1. **Testing is Sophisticated**: The project uses a 4-tier testing pyramid (Unit → Integration → Functional → Sandbox). Functional tests use **TestContainers** with dynamic port mapping across multiple LocalStack versions (v3.7.1, v4.3.0).
38+
39+
2. **Version-Aware Development**: The project carefully manages AWS SDK compatibility. Currently migrated to **AWS SDK v4** with specific considerations:
40+
- .NET Framework requirement bumped from 4.6.2 → 4.7.2
41+
- Extensions package uses new `ClientFactory<T>` pattern vs old non-generic `ClientFactory`
42+
- Some functional tests may fail due to behavioral changes in SNS/DynamoDB operations
43+
44+
3. **Enterprise Build System**: Uses **Cake Frosting** (not traditional Cake scripts) with cross-platform CI/CD across Ubuntu/Windows/macOS. The build system handles complex scenarios like .NET Framework testing on Mono.
45+
46+
4. **Service Coverage**: Supports **50+ AWS services** through intelligent endpoint resolution. Services are mapped through `AwsServiceEndpointMetadata.All` with both legacy per-service ports and modern unified edge port (4566).
47+
48+
5. **Reflection Strategy**: The codebase heavily uses reflection to access AWS SDK internals (private `serviceMetadata` fields, dynamic `ClientConfig` creation). This is being modernized with UnsafeAccessor for AOT.
49+
50+
### Core Architecture
51+
52+
The library follows a **Session-based architecture** with three main components:
53+
54+
1. **Session (`ISession`)**: Core client factory that configures AWS clients for LocalStack endpoints
55+
2. **Config (`IConfig`)**: Service endpoint resolution and LocalStack connection management
56+
3. **SessionReflection (`ISessionReflection`)**: Abstraction layer for AWS SDK private member access
57+
58+
### Key Innovation: Dual Reflection Strategy
59+
60+
The project uses a sophisticated **conditional compilation pattern** for .NET compatibility:
61+
62+
- **.NET 8+**: Uses **UnsafeAccessor** pattern via Roslyn source generators (`LocalStack.Client.Generators`) for Native AOT
63+
- **Legacy (.NET Standard 2.0, .NET Framework)**: Falls back to traditional reflection APIs
64+
65+
## Development Workflows
66+
67+
### Build System
68+
- **Build Scripts**: Use `build.ps1` (Windows) or `build.sh` (Linux) - these delegate to Cake build tasks
69+
- **Build Framework**: Uses **Cake Frosting** in `build/LocalStack.Build/` - examine `CakeTasks/` folder for available targets
70+
- **Common Commands**:
71+
- `build.ps1` - Full build and test
72+
- `build.ps1 --target=tests` - Run tests only
73+
74+
### Project Structure
75+
76+
```
77+
src/
78+
├── LocalStack.Client/ # Core library (multi-target: netstandard2.0, net472, net8.0, net9.0)
79+
├── LocalStack.Client.Extensions/ # DI integration (AddLocalStack() extension)
80+
└── LocalStack.Client.Generators/ # Source generator for AOT (netstandard2.0, Roslyn)
81+
82+
tests/
83+
├── LocalStack.Client.Tests/ # Unit tests (mocked)
84+
├── LocalStack.Client.Integration.Tests/ # Real LocalStack integration
85+
├── LocalStack.Client.AotCompatibility.Tests/ # Native AOT testing
86+
└── sandboxes/ # Example console apps
87+
```
88+
89+
## Critical Patterns & Conventions
90+
91+
### 1. Multi-Framework Configuration Pattern
92+
93+
For .NET 8+ conditional features, use this pattern consistently:
94+
95+
```csharp
96+
#if NET8_0_OR_GREATER
97+
// Modern implementation (UnsafeAccessor)
98+
var accessor = AwsAccessorRegistry.GetByInterface<TClient>();
99+
return accessor.CreateClient(credentials, clientConfig);
100+
#else
101+
// Legacy implementation (reflection)
102+
return CreateClientByInterface(typeof(TClient), useServiceUrl);
103+
#endif
104+
```
105+
106+
### 2. Session Client Creation Pattern
107+
108+
The Session class provides multiple client creation methods - understand the distinction:
109+
110+
- `CreateClientByInterface<IAmazonS3>()` - Interface-based (preferred)
111+
- `CreateClientByImplementation<AmazonS3Client>()` - Concrete type
112+
- `useServiceUrl` parameter controls endpoint vs region configuration
113+
114+
### 3. Source Generator Integration
115+
116+
When working on AOT features:
117+
- Generator runs only for .NET 8+ projects (`Net8OrAbove` condition)
118+
- Discovers AWS clients from referenced assemblies at compile-time
119+
- Generates `IAwsAccessor` implementations in `LocalStack.Client.Generated` namespace
120+
- Auto-registers via `ModuleInitializer` in `AwsAccessorRegistry`
121+
122+
### 4. Configuration Hierarchy
123+
124+
LocalStack configuration follows this pattern:
125+
```json
126+
{
127+
"LocalStack": {
128+
"UseLocalStack": true,
129+
"Session": {
130+
"RegionName": "eu-central-1",
131+
"AwsAccessKeyId": "accessKey",
132+
"AwsAccessKey": "secretKey"
133+
},
134+
"Config": {
135+
"LocalStackHost": "localhost.localstack.cloud",
136+
"EdgePort": 4566,
137+
"UseSsl": false
138+
}
139+
}
140+
}
141+
```
142+
143+
## Testing Patterns
144+
145+
### Multi-Layered Testing Strategy
146+
Based on the comprehensive testing guide in the onboarding document:
147+
148+
- **Unit Tests**: `MockSession.Create()` → Setup mocks → Verify calls pattern
149+
- **Integration Tests**: Client creation across **50+ AWS services** without external dependencies
150+
- **Functional Tests**: **TestContainers** with multiple LocalStack versions (v3.7.1, v4.3.0)
151+
- **Sandbox Apps**: Real-world examples in `tests/sandboxes/` demonstrating usage patterns
152+
153+
### TestContainers Pattern
154+
```csharp
155+
// Dynamic port mapping prevents conflicts
156+
ushort mappedPublicPort = localStackFixture.LocalStackContainer.GetMappedPublicPort(4566);
157+
```
158+
159+
### Known Testing Challenges
160+
- **SNS Issues**: LocalStack v3.7.2/v3.8.0 have known SNS bugs (use v3.7.1 or v3.9.0+)
161+
- **SQS Compatibility**: AWSSDK.SQS 3.7.300+ has issues with LocalStack v1/v2
162+
- **AWS SDK v4 Migration**: Some functional tests may fail due to behavioral changes
163+
164+
## Package Management
165+
166+
**Centralized Package Management** - Always follow this two-step process:
167+
168+
1. Add to `Directory.Packages.props`:
169+
```xml
170+
<PackageVersion Include="MyPackage" Version="1.0.0" />
171+
```
172+
173+
2. Reference in project:
174+
```xml
175+
<PackageReference Include="MyPackage" />
176+
```
177+
178+
### Code Quality Standards
179+
The onboarding document emphasizes **enterprise-level quality**:
180+
- **10+ Analyzers Active**: Roslynator, SonarAnalyzer, Meziantou.Analyzer, SecurityCodeScan
181+
- **Warnings as Errors**: `TreatWarningsAsErrors=true` across solution
182+
- **Nullable Reference Types**: Enabled solution-wide for safety
183+
- **Modern C# 13**: Latest language features with strict mode enabled
184+
185+
## Working with AWS SDK Integration
186+
187+
### Service Discovery Pattern
188+
The library auto-discovers AWS services using naming conventions:
189+
- `IAmazonS3``AmazonS3Client``AmazonS3Config`
190+
- Service metadata extracted from private static `serviceMetadata` fields
191+
- Endpoint mapping in `AwsServiceEndpointMetadata.All`
192+
193+
### Reflection Abstraction
194+
When adding AWS SDK integration features:
195+
- Always implement in both `SessionReflectionLegacy` and `SessionReflectionModern`
196+
- Modern version uses generated `IAwsAccessor` implementations
197+
- Legacy version uses traditional reflection with error handling
198+
199+
## Key Files to Understand
200+
201+
- `src/LocalStack.Client/Session.cs` - Core client factory logic
202+
- `src/LocalStack.Client/Utils/SessionReflection.cs` - Facade that chooses implementation
203+
- `src/LocalStack.Client.Generators/AwsAccessorGenerator.cs` - Source generator main logic
204+
- `tests/sandboxes/` - Working examples of all usage patterns
205+
- `Directory.Build.props` - Shared MSBuild configuration with analyzer rules
206+
207+
## Plan-Then-Execute Workflow
208+
209+
1. **Propose architectural approach** - especially for cross-framework features
210+
2. **Consider AOT implications** - will this work with UnsafeAccessor pattern?
211+
3. **Plan test strategy** - unit, integration, and AOT compatibility
212+
4. **Wait for approval** before implementing significant changes
213+
214+
The codebase prioritizes **backwards compatibility** and **AOT-first design** - keep these principles central to any contributions.

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ Localstack.NET is an easy-to-use .NET client for [LocalStack](https://github.com
3232
- [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard)
3333
- [.NET Framework 4.7.2 and Above](https://dotnet.microsoft.com/download/dotnet-framework)
3434

35+
## ⚡ Native AOT & Trimming Status
36+
37+
> **Heads‑up for `dotnet publish -p:PublishAot=true` / `PublishTrimmed=true` users**
38+
39+
- **v2.0.0 GA ships without Native AOT support.**
40+
The current build still relies on reflection for some AWS SDK internals.
41+
- Public entry points that touch reflection are tagged with
42+
`[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`.
43+
- You’ll see IL3050 / IL2026 warnings at compile time (promoted to errors in a strict AOT publish).
44+
- We ship the necessary linker descriptor with **`LocalStack.Client.Extensions`** to keep the private
45+
`ClientFactory<T>` members alive. No extra steps on your side.
46+
- Until the reflection‑free, source‑generated path lands (work in progress in
47+
[draft PR #49](https://github.com/localstack-dotnet/localstack-dotnet-client/pull/49) and tracked on
48+
[roadmap #48](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/48)):
49+
1. **Suppress** the warnings in your app *or* call the APIs that don’t rely on reflection.
50+
2. If you hit a runtime “missing member” error, ensure you’re on AWS SDK v4 **≥ 4.1.\*** and include the
51+
concrete `AWSSDK.*` package you’re instantiating.
52+
53+
> **Planned** – v2.1 will introduce an AOT‑friendly factory that avoids reflection entirely; once you
54+
> migrate to that API the warnings disappear.
55+
3556
### Build & Test Matrix
3657

3758
| Category | Platform/Type | Status | Description |

src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ public sealed class AwsClientFactoryWrapper : IAwsClientFactoryWrapper
88
private static readonly string ClientFactoryGenericTypeName = "Amazon.Extensions.NETCore.Setup.ClientFactory`1";
99
private static readonly string CreateServiceClientMethodName = "CreateServiceClient";
1010

11+
#if NET8_0_OR_GREATER
12+
[RequiresDynamicCode("Creates generic ClientFactory<T> and invokes internal members via reflection"),
13+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
14+
#endif
1115
public AmazonServiceClient CreateServiceClient<TClient>(IServiceProvider provider, AWSOptions? awsOptions) where TClient : IAmazonService
1216
{
1317
Type? genericFactoryType = typeof(ConfigurationException).Assembly.GetType(ClientFactoryGenericTypeName);

src/LocalStack.Client.Extensions/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
global using System;
2+
global using System.Diagnostics.CodeAnalysis;
23
global using System.Reflection;
34
global using System.Runtime.Serialization;
45

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<linker>
2+
<assembly fullname="Amazon.Extensions.NETCore.Setup">
3+
<!-- Preserve the whole generic type (safer and still small) -->
4+
<type fullname="Amazon.Extensions.NETCore.Setup.ClientFactory`1" preserve="all" />
5+
</assembly>
6+
</linker>

src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
2525
</PropertyGroup>
2626

27+
<ItemGroup>
28+
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
29+
</ItemGroup>
30+
2731
<ItemGroup>
2832
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup"/>
2933

src/LocalStack.Client/Session.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,28 @@ public Session(ISessionOptions sessionOptions, IConfig config, ISessionReflectio
1515
_sessionReflection = sessionReflection;
1616
}
1717

18+
#if NET8_0_OR_GREATER
19+
[RequiresDynamicCode("Uses Activator/CreateInstance and private‑field reflection; not safe for Native AOT."),
20+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
21+
#endif
1822
public TClient CreateClientByImplementation<TClient>(bool useServiceUrl = false) where TClient : AmazonServiceClient
1923
{
2024
Type clientType = typeof(TClient);
2125

2226
return (TClient)CreateClientByImplementation(clientType, useServiceUrl);
2327
}
2428

25-
public AmazonServiceClient CreateClientByImplementation(Type implType, bool useServiceUrl = false)
29+
#if NET8_0_OR_GREATER
30+
[RequiresDynamicCode("Uses Activator/CreateInstance and private‑field reflection; not safe for Native AOT."),
31+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
32+
#endif
33+
public AmazonServiceClient CreateClientByImplementation(
34+
#if NET8_0_OR_GREATER
35+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicFields)]Type implType,
36+
#else
37+
Type implType,
38+
#endif
39+
bool useServiceUrl = false)
2640
{
2741
if (!useServiceUrl && string.IsNullOrWhiteSpace(_sessionOptions.RegionName))
2842
{
@@ -55,14 +69,24 @@ public AmazonServiceClient CreateClientByImplementation(Type implType, bool useS
5569
return clientInstance;
5670
}
5771

72+
#if NET8_0_OR_GREATER
73+
[RequiresDynamicCode("Uses Activator/CreateInstance and private‑field reflection; not safe for Native AOT."),
74+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
75+
#endif
5876
public AmazonServiceClient CreateClientByInterface<TClient>(bool useServiceUrl = false) where TClient : IAmazonService
5977
{
6078
Type serviceInterfaceType = typeof(TClient);
6179

6280
return CreateClientByInterface(serviceInterfaceType, useServiceUrl);
6381
}
6482

65-
public AmazonServiceClient CreateClientByInterface(Type serviceInterfaceType, bool useServiceUrl = false)
83+
public AmazonServiceClient CreateClientByInterface(
84+
#if NET8_0_OR_GREATER
85+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicFields)]Type serviceInterfaceType,
86+
#else
87+
Type serviceInterfaceType,
88+
#endif
89+
bool useServiceUrl = false)
6690
{
6791
if (serviceInterfaceType == null)
6892
{

src/LocalStack.Client/Utils/SessionReflection.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ public IServiceMetadata ExtractServiceMetadata<TClient>() where TClient : Amazon
1010
return ExtractServiceMetadata(clientType);
1111
}
1212

13+
#if NET8_0_OR_GREATER
14+
[RequiresDynamicCode("Accesses private field 'serviceMetadata' with reflection; not safe for Native AOT."),
15+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
16+
#endif
1317
public IServiceMetadata ExtractServiceMetadata(Type clientType)
1418
{
1519
if (clientType == null)
@@ -33,6 +37,10 @@ public ClientConfig CreateClientConfig<TClient>() where TClient : AmazonServiceC
3337
return CreateClientConfig(clientType);
3438
}
3539

40+
#if NET8_0_OR_GREATER
41+
[RequiresDynamicCode("Uses Activator.CreateInstance on derived ClientConfig types; not safe for Native AOT."),
42+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
43+
#endif
3644
public ClientConfig CreateClientConfig(Type clientType)
3745
{
3846
if (clientType == null)
@@ -46,6 +54,10 @@ public ClientConfig CreateClientConfig(Type clientType)
4654
return (ClientConfig)Activator.CreateInstance(clientConfigParam.ParameterType);
4755
}
4856

57+
#if NET8_0_OR_GREATER
58+
[RequiresDynamicCode("Reflects over Config.RegionEndpoint property; not safe for Native AOT."),
59+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
60+
#endif
4961
public void SetClientRegion(AmazonServiceClient amazonServiceClient, string systemName)
5062
{
5163
if (amazonServiceClient == null)
@@ -59,6 +71,10 @@ public void SetClientRegion(AmazonServiceClient amazonServiceClient, string syst
5971
regionEndpointProperty?.SetValue(amazonServiceClient.Config, RegionEndpoint.GetBySystemName(systemName));
6072
}
6173

74+
#if NET8_0_OR_GREATER
75+
[RequiresDynamicCode("Reflects over ForcePathStyle property; not safe for Native AOT."),
76+
RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")]
77+
#endif
6278
public bool SetForcePathStyle(ClientConfig clientConfig, bool value = true)
6379
{
6480
if (clientConfig == null)

0 commit comments

Comments
 (0)