diff --git a/.github/workflows/aot-audit.yml b/.github/workflows/aot-audit.yml
new file mode 100644
index 000000000..aa268a4b2
--- /dev/null
+++ b/.github/workflows/aot-audit.yml
@@ -0,0 +1,80 @@
+name: Ecng Native AOT audit
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - 'common.props'
+ - 'Common/**'
+ - 'Serialization/**'
+ - 'Tests/Common/AotCompatibilityTests.cs'
+ - 'AotSmoke/**'
+ - '.github/workflows/aot-audit.yml'
+
+jobs:
+ aot:
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Setup .NET 10
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: |
+ 6.0.x
+ 10.0.x
+
+ - name: Install native AOT prerequisites
+ run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev
+
+ - name: Compile legacy net6 target
+ run: dotnet build Serialization/Serialization.csproj -c Release -f net6.0
+
+ - name: Analyze Ecng.Common
+ shell: bash
+ run: |
+ set -o pipefail
+ dotnet build Common/Common.csproj -c Release -f net10.0 \
+ -p:EcngAotAudit=true -p:TrimmerSingleWarn=false \
+ -bl:common-aot.binlog 2>&1 | tee common-aot.log
+
+ - name: Analyze Ecng.Serialization
+ shell: bash
+ run: |
+ set -o pipefail
+ dotnet build Serialization/Serialization.csproj -c Release -f net10.0 \
+ -p:EcngAotAudit=true -p:TrimmerSingleWarn=false \
+ -bl:serialization-aot.binlog 2>&1 | tee serialization-aot.log
+
+ - name: Run compatibility tests
+ run: >
+ dotnet test Tests/Tests.csproj -c Release -f net10.0
+ --filter 'FullyQualifiedName~AotCompatibilityTests'
+ --settings Tests/test.runsettings
+
+ - name: Publish native smoke app
+ shell: bash
+ run: |
+ set -o pipefail
+ dotnet publish AotSmoke/AotSmoke.csproj -f net10.0 -c Release -r linux-x64 \
+ -p:EcngAotAudit=true -p:TrimmerSingleWarn=false \
+ -bl:publish-aot.binlog -o artifacts/ecng-aot 2>&1 | tee publish-aot.log
+
+ - name: Execute native smoke app
+ run: ./artifacts/ecng-aot/Ecng.AotSmoke
+
+ - name: Upload audit logs
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: ecng-aot-diagnostics
+ path: |
+ common-aot.log
+ common-aot.binlog
+ serialization-aot.log
+ serialization-aot.binlog
+ publish-aot.log
+ publish-aot.binlog
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/AotSmoke/AotSmoke.csproj b/AotSmoke/AotSmoke.csproj
new file mode 100644
index 000000000..a765b02f8
--- /dev/null
+++ b/AotSmoke/AotSmoke.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ true
+ false
+ false
+ false
+
+
+
+
+
+
diff --git a/AotSmoke/Program.cs b/AotSmoke/Program.cs
new file mode 100644
index 000000000..a8c6ed776
--- /dev/null
+++ b/AotSmoke/Program.cs
@@ -0,0 +1,130 @@
+namespace Ecng.AotSmoke;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Text.Json.Serialization;
+
+using Ecng.Common;
+using Ecng.Serialization;
+using Ecng.Reflection;
+
+using Newtonsoft.Json;
+
+internal sealed record QuoteSnapshot(decimal Price, decimal Volume);
+
+[JsonSerializable(typeof(QuoteSnapshot))]
+internal partial class AotJsonContext : JsonSerializerContext
+{
+}
+
+#pragma warning disable CS0618 // Test compatibility with existing persistable settings storage.
+internal sealed class StoredQuote : IPersistable
+{
+ public int Quantity { get; private set; }
+ public void Load(SettingsStorage storage) => Quantity = storage.GetValue("quantity");
+ public void Save(SettingsStorage storage) => storage.Set("quantity", Quantity);
+}
+#pragma warning restore CS0618
+
+internal interface IDiscoveryProbe { }
+internal sealed class DiscoveryProbe : IDiscoveryProbe { }
+
+internal sealed class WireQuoteRow
+{
+ public decimal Price;
+ public decimal Volume;
+}
+
+internal sealed class PrivateAotCtor
+{
+ private PrivateAotCtor() { }
+ public int Value { get; private set; }
+ public static PrivateAotCtor Create(int value) => new() { Value = value };
+}
+
+internal static class Program
+{
+ private static async Task Main()
+ {
+ // Static references ensure these generic instantiations exist in the native binary.
+ TypeHelper.RegisterConstructedGenericType(typeof(Dictionary));
+ var genericType = typeof(Dictionary<,>).Make(typeof(string), typeof(QuoteSnapshot));
+ if (genericType != typeof(Dictionary))
+ throw new InvalidOperationException("Closed generic type lookup failed.");
+
+ TypeHelper.RegisterFactory(args => new QuoteSnapshot((decimal)args[0], (decimal)args[1]));
+ var quote = typeof(QuoteSnapshot).CreateInstance(100m, 2m);
+ if (quote.Price != 100m || quote.Volume != 2m)
+ throw new InvalidOperationException("Registered factory failed.");
+
+ Converter.RegisterTypeName("ecng.aot.quote");
+ if ("ECNG.AOT.QUOTE".To() != typeof(QuoteSnapshot))
+ throw new InvalidOperationException("Registered type name failed.");
+
+ var json = quote.ToJson(AotJsonContext.Default.QuoteSnapshot);
+ var copy = json.FromJson(AotJsonContext.Default.QuoteSnapshot);
+ if (copy != quote)
+ throw new InvalidOperationException("Source-generated JSON round trip failed.");
+
+ ReflectionHelper.RegisterImplementation();
+ var implementations = typeof(DiscoveryProbe).Assembly
+ .FindImplementations(showNonPublic: true);
+ if (!implementations.Contains(typeof(DiscoveryProbe)))
+ throw new InvalidOperationException("AOT implementation discovery failed.");
+
+ // Existing array allocation and generic construction paths, not only new APIs.
+ ArrayHelper.RegisterArrayType();
+ var typedArray = typeof(QuoteSnapshot).CreateArray(2);
+ if (typedArray is not QuoteSnapshot[] || typedArray.Length != 2)
+ throw new InvalidOperationException("Registered array allocation failed.");
+
+ if (typeof(int).CreateArray(3) is not int[] { Length: 3 })
+ throw new InvalidOperationException("Standard array factory failed.");
+ TypeHelper.RegisterConstructedGenericType(typeof(List));
+ TypeHelper.RegisterFactory>(_ => new List());
+ var listType = typeof(List<>).Make(typeof(int));
+ if (listType.CreateInstance() is not List)
+ throw new InvalidOperationException("Registered list factory failed.");
+
+ FastActivator.RegisterFactory(() => PrivateAotCtor.Create(9));
+ if (FastActivator.CreateObject().Value != 9)
+ throw new InvalidOperationException("FastActivator override failed.");
+
+ TypeHelper.RegisterFactory(_ => new StoredQuote());
+ var storage = new SettingsStorage()
+ .Set("quote", new SettingsStorage().Set("quantity", 7));
+ if (storage.GetValue("quote").Quantity != 7)
+ throw new InvalidOperationException("Existing SettingsStorage factory path failed.");
+
+ // Exercise the existing Ecng JSON collection deserializer with the registered
+ // List and array factory. This uses the legacy wire format on purpose.
+ var legacySerializer = new Ecng.Serialization.JsonSerializer>();
+ using var stream = new MemoryStream();
+ await legacySerializer.SerializeAsync(new List { 1, 2, 3 }, stream, default);
+ stream.Position = 0;
+ var restoredList = await legacySerializer.DeserializeAsync(stream, default);
+ if (restoredList.Count != 3 || restoredList[0] != 1 || restoredList[2] != 3)
+ throw new InvalidOperationException("Existing collection serializer path failed.");
+
+ // The legacy Newtonsoft field setter must not attempt Reflection.Emit on AOT.
+ JArrayToObjectConverter.RegisterAotFields(
+ (typeof(decimal), row => row.Price, (row, value) => row.Price = (decimal)value),
+ (typeof(decimal), row => row.Volume, (row, value) => row.Volume = (decimal)value));
+ using var reader = new JsonTextReader(new StringReader("[101.25,2.5]"));
+ reader.Read();
+ var row = (WireQuoteRow)new JArrayToObjectConverter()
+ .ReadJson(reader, typeof(WireQuoteRow), null, Newtonsoft.Json.JsonSerializer.CreateDefault());
+ if (row.Price != 101.25m || row.Volume != 2.5m)
+ throw new InvalidOperationException("Legacy JSON-array field setter path failed.");
+
+ var dynamicTypeInfo = (System.Text.Json.Serialization.Metadata.JsonTypeInfo)AotJsonContext.Default.QuoteSnapshot;
+ var metadataJson = ((object)quote).ToJson(dynamicTypeInfo);
+ if ((QuoteSnapshot)metadataJson.FromJson(dynamicTypeInfo) != quote)
+ throw new InvalidOperationException("Non-generic source-generated JSON failed.");
+
+ Console.WriteLine("Ecng Native AOT smoke test passed.");
+ }
+}
diff --git a/AotSmoke/README.md b/AotSmoke/README.md
new file mode 100644
index 000000000..50fabfc95
--- /dev/null
+++ b/AotSmoke/README.md
@@ -0,0 +1,36 @@
+# Ecng Native AOT compatibility audit
+
+The core Ecng libraries keep their normal JIT behavior and NuGet package compatibility.
+
+## Compile-time diagnostics
+
+Run the following commands from the repository root:
+
+```bash
+dotnet build Common/Common.csproj -c Release -f net10.0 -p:EcngAotAudit=true
+dotnet build Serialization/Serialization.csproj -c Release -f net10.0 -p:EcngAotAudit=true
+```
+
+This opt-in flag enables AOT, trimming and single-file analyzers on net10.0 builds.
+It does not declare that the libraries are globally AOT-compatible.
+
+## Minimal native smoke application
+
+```bash
+dotnet publish AotSmoke/AotSmoke.csproj -f net10.0 -c Release -r linux-x64 -p:EcngAotAudit=true
+./AotSmoke/bin/Release/net10.0/linux-x64/publish/Ecng.AotSmoke
+```
+
+The smoke app requires Linux Clang and zlib development headers. It demonstrates:
+
+- Factory registration with `TypeHelper.RegisterFactory`
+- Closed generic registration with `TypeHelper.RegisterConstructedGenericType`
+- Name-to-type registration with `Converter.RegisterTypeName`
+- Source-generated System.Text.Json through `JsonHelper.FromJson/ToJson` overloads
+
+Old Newtonsoft.Json and dynamic reflection helpers remain available to ordinary JIT applications.
+For an AOT process, types must be referenced and registered at build time; arbitrary plugin DLL loading,
+dynamic IL compilation and unregistered generic instantiations are not supported by these helpers.
+
+Successful compilation and a passing smoke test do not establish AOT compatibility
+of the full Ecng framework. Review the full IL2xxx/IL3xxx warning log before expanding coverage.
diff --git a/Common/ArrayHelper.cs b/Common/ArrayHelper.cs
index 7ee3d0a5c..f126387e2 100644
--- a/Common/ArrayHelper.cs
+++ b/Common/ArrayHelper.cs
@@ -1,10 +1,58 @@
namespace Ecng.Common;
+using System.Collections.Concurrent;
+
///
/// Utility class for common array operations.
///
public static class ArrayHelper
{
+ private static readonly ConcurrentDictionary> _registeredArrayFactories = new();
+
+ static ArrayHelper()
+ {
+ // Common arrays exist as closed generic instantiations in the native binary.
+ // Avoid Array.CreateInstance(Type, int) for standard wire/message value types.
+ RegisterArrayType