Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .github/workflows/aws-sdk-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
global-json-file: global.json

Expand Down
16 changes: 0 additions & 16 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,9 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
global-json-file: global.json

# The macOS runner image no longer ships Mono, and VSTest needs the real 'mono' host
# on the PATH to execute the net472 test assemblies.
- name: "Install Mono (macOS)"
if: runner.os == 'macOS'
run: |
if command -v mono >/dev/null 2>&1; then
echo "✅ Mono already present: $(mono --version | head -1)"
else
brew install mono
echo "✅ Installed: $(mono --version | head -1)"
fi

- name: "Cache NuGet packages"
uses: actions/cache@v6
with:
Expand Down Expand Up @@ -169,8 +155,6 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
global-json-file: global.json

Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
global-json-file: global.json

Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ See v1.x change log for previous versions: [CHANGELOG.md](https://github.com/loc

### General

- Replaced the AWS-mode wrapper's private SDK reflection with the public `AddAWSService<T>` factory, preserving provider options fallback and client lifetimes. Removed its obsolete linker descriptor and reflection/AOT annotations; Core client creation and `AddLocalStack` configuration binding still do not support Native AOT.
- Replaced the AWS-mode wrapper's private SDK reflection with the public `AddAWSService<T>` factory, preserving provider options fallback and client lifetimes. Removed its obsolete linker descriptor and reflection/AOT annotations.
- Added automatic Core source generation that roots existing reflection operations, plus lazy fixed-schema configuration binding, preserving existing C# `AddLocalStack`, AWS service registration, and Core `Session` calls. Local-package Native AOT validation covers .NET 10 on `win-x64` and `linux-x64`, including S3 requests and endpoint, region, credentials, and configured-value parity with JIT; see the [scope boundary](docs/architecture/platform-and-packaging.md#native-aot-and-trimming).
- Added `Microsoft.Extensions.Options` 8.0.2 only to modern Extensions assets for Native AOT, implying `Microsoft.Extensions.Primitives >= 8.0.0` there. Legacy dependency floors and existing intentional minimums are unchanged.
- Added explicit `net10.0` target assets to both packages while retaining their existing target frameworks.
- Set the development SDK baseline to 10.0.402 and configured test projects and CI workflows to cover `net10.0`.
- Updated all existing non-Core centrally managed NuGet versions to their latest stable releases, including build, test, and analyzer packages (Extensions' consumer minimums are preserved below). Build-tool updates include Cake.Frosting 6.3.0 and NuGet.Versioning 7.9.0.
Expand Down
25 changes: 25 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,31 @@
<!-- Floating versions are opt-in under central package management (NU1011). -->
<CentralPackageFloatingVersionsEnabled Condition="'$(AwsSetupTrack)' == 'latest'">true</CentralPackageFloatingVersionsEnabled>

<!-- Target frameworks. "Modern" targets get Native AOT support, the source generator and the
reflection-free client creation paths; add a new .NET version here and everything follows. -->
<LocalStackModernMinimumTargetFramework>net10.0</LocalStackModernMinimumTargetFramework>
<LocalStackModernTargetFrameworks>$(LocalStackModernMinimumTargetFramework);net11.0</LocalStackModernTargetFrameworks>
<LocalStackNetStandardTargetFramework>netstandard2.0</LocalStackNetStandardTargetFramework>
<LocalStackNetFrameworkTargetFramework>net472</LocalStackNetFrameworkTargetFramework>

<LocalStackClientTargetFrameworks>$(LocalStackNetStandardTargetFramework);$(LocalStackNetFrameworkTargetFramework);$(LocalStackModernTargetFrameworks)</LocalStackClientTargetFrameworks>
<LocalStackExtensionsTargetFrameworks>$(LocalStackNetStandardTargetFramework);$(LocalStackModernTargetFrameworks)</LocalStackExtensionsTargetFrameworks>
<!-- Test projects that also run on .NET Framework. -->
<LocalStackNetFrameworkAndModernTargetFrameworks>$(LocalStackNetFrameworkTargetFramework);$(LocalStackModernTargetFrameworks)</LocalStackNetFrameworkAndModernTargetFrameworks>

<!-- Package validation compares each package with this published version. -->
<LocalStackPackageValidationBaselineVersion>2.0.0</LocalStackPackageValidationBaselineVersion>
<!-- Assets the baseline has and this release drops on purpose (.NET 8/9 end of support; those apps use netstandard2.0). -->
<LocalStackPackageValidationDroppedTargetFrameworks>net8.0;net9.0</LocalStackPackageValidationDroppedTargetFrameworks>

</PropertyGroup>

<!-- True in the inner build of a modern target framework; false elsewhere (including the outer build). -->
<PropertyGroup>
<IsLocalStackModernTarget>false</IsLocalStackModernTarget>
<IsLocalStackModernTarget Condition="'$(TargetFramework)' != '' and $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', '$(LocalStackModernMinimumTargetFramework)'))">true</IsLocalStackModernTarget>
<IsLocalStackNetFrameworkTarget>false</IsLocalStackNetFrameworkTarget>
<IsLocalStackNetFrameworkTarget Condition="'$(TargetFramework)' == '$(LocalStackNetFrameworkTargetFramework)'">true</IsLocalStackNetFrameworkTarget>
</PropertyGroup>

<!-- third-party analyzers -->
Expand Down
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="4.*" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
<!-- third-party analyzers -->
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.312" />
Expand Down Expand Up @@ -186,4 +188,4 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="18.11.0" />
</ItemGroup>
</Project>
</Project>
4 changes: 3 additions & 1 deletion LocalStack.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<Project Path="build\LocalStack.Build\LocalStack.Build.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="tests\LocalStack.Client.Generators.Tests\LocalStack.Client.Generators.Tests.csproj" />
<Project Path="tests\LocalStack.Client.Extensions.Tests\LocalStack.Client.Extensions.Tests.csproj" />
<Project Path="tests\LocalStack.Client.Functional.Tests\LocalStack.Client.Functional.Tests.csproj" />
<Project Path="tests\LocalStack.Client.Integration.Tests\LocalStack.Client.Integration.Tests.csproj" />
Expand All @@ -27,4 +28,5 @@
</Folder>
<Project Path="src\LocalStack.Client.Extensions\LocalStack.Client.Extensions.csproj" />
<Project Path="src\LocalStack.Client\LocalStack.Client.csproj" />
</Solution>
<Project Path="src\LocalStack.Client.Generators\LocalStack.Client.Generators.csproj" />
</Solution>
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ LocalStack.NET is a .NET client library for [LocalStack](https://github.com/loca

## ⚡ Native AOT & Trimming Status

> **Heads‑up for `dotnet publish -p:PublishAot=true` / `PublishTrimmed=true` users**

- **The current v2 packages do not support Native AOT.** Core client creation still uses reflection over AWS SDK internals, and Extensions' configuration binding uses non-public setters. The AWS-mode wrapper's public SDK factory path does not establish AOT compatibility for the complete application. Respect `[RequiresDynamicCode]` and `[RequiresUnreferencedCode]` annotations where present; do not suppress them to obtain a successful publish.
- The reflection-free source-generator work is in [draft PR #49](https://github.com/localstack-dotnet/localstack-dotnet-client/pull/49), not in `master`. Do not suppress trimming/AOT warnings or adopt an unverified service descriptor on the assumption that this draft has shipped. See the [platform boundary](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/master/docs/architecture/platform-and-packaging.md).
- **Unreleased:** Native AOT support preserves existing C# calls such as `AddLocalStack(configuration)`, `AddAwsService<IAmazonS3>()`, and Core `Session` calls. Source generation flows automatically through either package; no new registration or factory API is required. It roots existing reflection operations rather than removing reflection.
- Local-package consumers have been validated with Native AOT and its mandatory trimming on .NET 10 for `win-x64` and `linux-x64`, including S3 `ListBucketsAsync` and JIT parity for endpoint, region, credentials, and configured values. This does not establish support for every service, operation, or native target, or for separately trimmed JIT deployments. Do not suppress trim/AOT warnings.
- This implementation is **not part of the published Core 2.0.0 / Extensions 2.0.1 releases**. See the [platform boundary](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/master/docs/architecture/platform-and-packaging.md#native-aot-and-trimming) for scope and the [Native AOT decision](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/master/docs/adr/0004-native-aot-preserves-existing-consumer-code.md) for the unchanged-consumer contract.

### Build & Test Matrix

Expand Down
44 changes: 34 additions & 10 deletions build/LocalStack.Build/BuildContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,21 +385,45 @@ private static string ToPreReleaseIdentifier(string identifier)
return NuGetVersion.TryParse($"0.0.0-{sanitised}", out NuGetVersion? _) ? sanitised : $"g{sanitised}";
}

private string[] GetProjectTargetFrameworks(string csprojPath)
// Asks MSBuild for the evaluated frameworks, so lists built from Directory.Build.props properties resolve.
private static string[] GetProjectTargetFrameworks(string csprojPath)
{
FilePath file = this.File(csprojPath);
string project = File.ReadAllText(file.FullPath, Encoding.UTF8);
string dotnetHost = System.Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ??
throw new InvalidOperationException("DOTNET_HOST_PATH is not set; run the build through the dotnet CLI.");

bool multipleFrameworks = project.Contains("<TargetFrameworks>");
string startElement = multipleFrameworks ? "<TargetFrameworks>" : "<TargetFramework>";
string endElement = multipleFrameworks ? "</TargetFrameworks>" : "</TargetFramework>";
var startInfo = new System.Diagnostics.ProcessStartInfo(dotnetHost)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
startInfo.ArgumentList.Add("msbuild");
startInfo.ArgumentList.Add(csprojPath);
startInfo.ArgumentList.Add("-getProperty:TargetFrameworks");
startInfo.ArgumentList.Add("-getProperty:TargetFramework");

using System.Diagnostics.Process process = System.Diagnostics.Process.Start(startInfo) ??
throw new InvalidOperationException($"Could not start '{dotnetHost} msbuild'.");
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();

if (process.ExitCode != 0)
{
throw new InvalidOperationException($"'dotnet msbuild {csprojPath}' exited with code {process.ExitCode}: {output}{error}");
}

int startIndex = project.IndexOf(startElement, StringComparison.Ordinal) + startElement.Length;
int endIndex = project.IndexOf(endElement, startIndex, StringComparison.Ordinal);
using var document = System.Text.Json.JsonDocument.Parse(output);
System.Text.Json.JsonElement properties = document.RootElement.GetProperty("Properties");
string? frameworks = properties.GetProperty("TargetFrameworks").GetString();

string targetFrameworks = project[startIndex..endIndex];
if (string.IsNullOrWhiteSpace(frameworks))
{
frameworks = properties.GetProperty("TargetFramework").GetString();
}

return targetFrameworks.Split(';');
return (frameworks ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}

private string GetAssemblyName(string csprojPath)
Expand Down
9 changes: 4 additions & 5 deletions build/LocalStack.Build/CakeTasks/TestTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,12 @@ public override void Run(BuildContext context)
settings.Filter = null;
}

// .NET Framework testing on non-Windows platforms
// - Modern .NET includes built-in Mono runtime
// - Test platform still requires external Mono installation on Linux
// The .NET 11 SDK's test platform runs .NET Framework tests on Windows only; Mono is no longer supported.
if (targetFramework == "net472" && !context.IsRunningOnWindows())
{
string platform = context.IsRunningOnLinux() ? "Linux (with external Mono)" : "macOS (built-in Mono)";
ConsoleHelper.WriteInfo($"Running .NET Framework tests on {platform}");
ConsoleHelper.WriteWarning($"Skipping {targetFramework} tests for {testProj.AssemblyName}: .NET Framework tests run on Windows only");

continue;
}

string testFilePrefix = targetFramework.Replace('.', '-');
Expand Down
2 changes: 1 addition & 1 deletion build/LocalStack.Build/LocalStack.Build.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>$(LocalStackModernMinimumTargetFramework)</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<LangVersion>latest</LangVersion>
<NoWarn>
Expand Down
8 changes: 5 additions & 3 deletions docs/architecture/client-runtime.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Client runtime and service mapping

Date: 2026-10-17
Date: 2026-10-22

The core package creates AWS SDK service clients configured for LocalStack. The [glossary](../../GLOSSARY.md) distinguishes an AWS service, an SDK client, and a configured LocalStack endpoint.

Expand All @@ -12,8 +12,10 @@ The catalog is a mapping in **this library**, not a list of all AWS operations a

## Client creation

[`Session`](../../src/LocalStack.Client/Session.cs) accepts session options, an `IConfig` built from the endpoint catalog, and [`SessionReflection`](../../src/LocalStack.Client/Utils/SessionReflection.cs). Its interface-based path locates the AWS SDK implementation type by convention; its implementation-based path starts from the concrete type. Both inspect the SDK's private `serviceMetadata`, construct the corresponding `ClientConfig`, and create an SDK client with the configured credentials.
[`Session`](../../src/LocalStack.Client/Session.cs) retains its three-argument constructor accepting `ISessionOptions`, `IConfig`, and an injected `ISessionReflection`. Its interface-based path locates the AWS SDK implementation type by convention; its implementation-based path starts from the concrete type. Both call the injected helper to obtain service metadata and construct the corresponding `ClientConfig`, then create an SDK client with the configured credentials. The default [`SessionReflection`](../../src/LocalStack.Client/Utils/SessionReflection.cs) inspects the SDK's private `serviceMetadata`.

The client configuration sets `UseHttp`, `ProxyHost`, and `ProxyPort` from the LocalStack settings. Where the configuration exposes `ForcePathStyle`, the reflection helper sets it for S3-compatible addressing. With `useServiceUrl: true`, `Session` sets the explicit `ServiceURL`; otherwise it requires a region name and sets `RegionEndpoint`. These are two configuration paths for a LocalStack-created client, **not** the application-level LocalStack/AWS mode switch. They do not by themselves prove where every later request travels.

[`SessionStandalone`](../../src/LocalStack.Client/SessionStandalone.cs) creates this session without dependency injection. The optional Extensions package owns the application-level mode switch in [Extensions and composition](extensions-and-composition.md). The Core and Extensions reflection paths remain present on current `master`; see [platform and packaging](platform-and-packaging.md) for the Native AOT boundary.
On modern .NET assets, the unreleased implementation automatically generates rooted reflection operations for client and configuration types discovered in C# source and references, including closed types found in declaration signatures. An internal registry uses actual runtime type identity. Initialization stores delegates only: metadata access, configuration creation, property mutation, and client construction remain lazy at their original call sites. Generated hits are used on JIT too; dynamic-code-supported misses and legacy assets retain reflection fallbacks. This preserves injected helper calls, their ordering, metadata/configuration identity, and reflection constructor/property behavior rather than bypassing them with a separate factory path. It does not enable arbitrary unseen plugins to be loaded under Native AOT.

[`SessionStandalone`](../../src/LocalStack.Client/SessionStandalone.cs) creates this session without dependency injection. The optional Extensions package owns the application-level mode switch in [Extensions and composition](extensions-and-composition.md). See [platform and packaging](platform-and-packaging.md#native-aot-and-trimming) for the unreleased Native AOT validation boundary and automatic compiler-asset packaging.
Loading
Loading