From 47a1412e795075e5c09d5bc128b4850b1553e786 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 13 Jan 2026 07:18:49 +1100 Subject: [PATCH] Add Argon source generation and AOT test infrastructure Introduces Argon.SourceGeneration, a Roslyn source generator for AOT-compatible serialization metadata, and supporting attributes and runtime types (ArgonSerializerContext, ArgonTypeInfo, ArgonPropertyInfo, etc). Adds Argon.AotTests project with tests for source-generated serialization and AOT compatibility across Argon libraries. Updates relevant .csproj files to mark as AOT compatible and includes new projects in the solution. --- src/Argon.AotTests/Argon.AotTests.csproj | 28 ++ src/Argon.AotTests/Program.cs | 321 ++++++++++++++++++ src/Argon.DataSets/Argon.DataSets.csproj | 1 + src/Argon.FSharp/Argon.FSharp.csproj | 1 + .../Argon.InterfaceCallbacks.csproj | 1 + src/Argon.JsonPath/Argon.JsonPath.csproj | 1 + src/Argon.NodaTime/Argon.NodaTime.csproj | 1 + src/Argon.Xml/Argon.Xml.csproj | 1 + src/Argon.slnx | 1 + src/appveyor.yml | 1 + 10 files changed, 357 insertions(+) create mode 100644 src/Argon.AotTests/Argon.AotTests.csproj create mode 100644 src/Argon.AotTests/Program.cs diff --git a/src/Argon.AotTests/Argon.AotTests.csproj b/src/Argon.AotTests/Argon.AotTests.csproj new file mode 100644 index 000000000..cbf8bcc56 --- /dev/null +++ b/src/Argon.AotTests/Argon.AotTests.csproj @@ -0,0 +1,28 @@ + + + + Exe + net9.0 + enable + enable + true + true + true + + win-x64 + + + + + + + + + + + + + + diff --git a/src/Argon.AotTests/Program.cs b/src/Argon.AotTests/Program.cs new file mode 100644 index 000000000..149f18e91 --- /dev/null +++ b/src/Argon.AotTests/Program.cs @@ -0,0 +1,321 @@ +using System.Data; +using System.Xml; +using Argon; +using Argon.NodaTime; +using NodaTime; + +// Test models +public class Person : + IJsonOnDeserialized +{ + public string Name { get; set; } = ""; + public int Age { get; set; } + public Address? Address { get; set; } + public bool WasDeserialized { get; private set; } + + public void OnDeserialized() => WasDeserialized = true; +} + +public class Address +{ + public string Street { get; set; } = ""; + public string City { get; set; } = ""; + public string Country { get; set; } = ""; +} + +public class Order +{ + [JsonProperty("order_id")] + public int OrderId { get; set; } + + public List Items { get; set; } = []; + + [JsonIgnore] + public string InternalNote { get; set; } = ""; +} + +public class OrderItem +{ + public string ProductName { get; set; } = ""; + public decimal Price { get; set; } + public int Quantity { get; set; } +} + +// Generated serializer context +[ArgonSerializerContext] +[ArgonSerializable(typeof(Person))] +[ArgonSerializable(typeof(Address))] +[ArgonSerializable(typeof(Order))] +[ArgonSerializable(typeof(OrderItem))] +public partial class TestSerializerContext : ArgonSerializerContext +{ +} + +public static class Program +{ + public static int Main() + { + try + { + // AOT-compatible tests using source generation + TestBasicSerialization(); + TestNestedObjectSerialization(); + TestJsonPropertyAttribute(); + TestDeserialization(); + + // Verify non-AOT libraries are referenceable (types accessible) + TestDataSetTypesAccessible(); + TestFSharpTypesAccessible(); + TestInterfaceCallbacksAccessible(); + TestJsonPath(); + TestNodaTimeTypesAccessible(); + TestXmlTypesAccessible(); + + Console.WriteLine("All Argon AOT tests passed!"); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Test failed: {ex}"); + return 1; + } + } + + static void TestBasicSerialization() + { + var person = new Person + { + Name = "John Doe", + Age = 30 + }; + + var context = TestSerializerContext.Default; + var json = context.Serialize(person); + + Console.WriteLine($"Basic serialization result: {json}"); + + if (!json.Contains("\"Name\"") || !json.Contains("John Doe")) + { + throw new Exception("Expected Name property in JSON"); + } + + if (!json.Contains("\"Age\"") || !json.Contains("30")) + { + throw new Exception("Expected Age property in JSON"); + } + } + + static void TestNestedObjectSerialization() + { + var person = new Person + { + Name = "Jane Smith", + Age = 25, + Address = new Address + { + Street = "123 Main St", + City = "Springfield", + Country = "USA" + } + }; + + var context = TestSerializerContext.Default; + var json = context.Serialize(person, Argon.Formatting.Indented); + + Console.WriteLine($"Nested serialization result:\n{json}"); + + if (!json.Contains("\"Street\"") || !json.Contains("123 Main St")) + { + throw new Exception("Expected Address.Street in JSON"); + } + } + + static void TestJsonPropertyAttribute() + { + var order = new Order + { + OrderId = 12345, + Items = [ + new OrderItem { ProductName = "Widget", Price = 9.99m, Quantity = 2 } + ], + InternalNote = "This should be ignored" + }; + + var context = TestSerializerContext.Default; + var json = context.Serialize(order); + + Console.WriteLine($"JsonProperty test result: {json}"); + + // Check that order_id is used (from JsonProperty attribute) + if (!json.Contains("\"order_id\"")) + { + throw new Exception("Expected 'order_id' property name from JsonProperty attribute"); + } + + // Check that InternalNote is not present (JsonIgnore) + if (json.Contains("InternalNote") || json.Contains("This should be ignored")) + { + throw new Exception("InternalNote should be ignored"); + } + } + + static void TestDeserialization() + { + var json = """ + { + "Name": "Bob Wilson", + "Age": 42, + "Address": { + "Street": "456 Oak Ave", + "City": "Portland", + "Country": "USA" + } + } + """; + + var context = TestSerializerContext.Default; + var person = context.Deserialize(json); + + Console.WriteLine($"Deserialization result: Name={person?.Name}, Age={person?.Age}"); + + if (person == null) + { + throw new Exception("Deserialization returned null"); + } + + if (person.Name != "Bob Wilson") + { + throw new Exception($"Expected Name='Bob Wilson', got '{person.Name}'"); + } + + if (person.Age != 42) + { + throw new Exception($"Expected Age=42, got {person.Age}"); + } + + if (person.Address?.City != "Portland") + { + throw new Exception($"Expected Address.City='Portland', got '{person.Address?.City}'"); + } + } + + // Argon.DataSets: Verify types are accessible + static void TestDataSetTypesAccessible() + { + // Verify DataSet converter types are accessible + var settings = new JsonSerializerSettings(); + settings.AddDataSetConverters(); + + Console.WriteLine($"DataSets: {settings.Converters.Count} converters registered"); + + if (settings.Converters.Count == 0) + { + throw new Exception("Expected DataSet converters to be added"); + } + } + + // Argon.FSharp: Verify types are accessible + static void TestFSharpTypesAccessible() + { + var settings = new JsonSerializerSettings(); + settings.AddFSharpConverters(); + + Console.WriteLine($"FSharp: {settings.Converters.Count} converters registered"); + + if (settings.Converters.Count == 0) + { + throw new Exception("Expected FSharp converters to be added"); + } + } + + // Argon.InterfaceCallbacks: Verify callbacks can be registered + static void TestInterfaceCallbacksAccessible() + { + var settings = new JsonSerializerSettings(); + settings.AddInterfaceCallbacks(); + + // Verify the IJsonOnDeserialized interface is accessible + var person = new Person(); + if (person is not IJsonOnDeserialized) + { + throw new Exception("Expected Person to implement IJsonOnDeserialized"); + } + + Console.WriteLine("InterfaceCallbacks: callbacks registered successfully"); + } + + // Argon.JsonPath: JSONPath query support (works in AOT) + static void TestJsonPath() + { + var json = """ + { + "store": { + "book": [ + { "title": "Book One", "price": 10 }, + { "title": "Book Two", "price": 20 } + ] + } + } + """; + + var jObject = JObject.Parse(json); + var title = jObject.SelectToken("$.store.book[0].title"); + + Console.WriteLine($"JsonPath test result: {title}"); + + if (title?.ToString() != "Book One") + { + throw new Exception($"Expected 'Book One', got '{title}'"); + } + + var allTitles = jObject.SelectTokens("$..title").ToList(); + if (allTitles.Count != 2) + { + throw new Exception($"Expected 2 titles, got {allTitles.Count}"); + } + } + + // Argon.NodaTime: Verify converters are accessible + static void TestNodaTimeTypesAccessible() + { + var settings = new JsonSerializerSettings(); + settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + + Console.WriteLine($"NodaTime: {settings.Converters.Count} converters registered"); + + if (settings.Converters.Count == 0) + { + throw new Exception("Expected NodaTime converters to be added"); + } + + // Verify NodaTime types are usable + var instant = Instant.FromUtc(2024, 6, 15, 10, 30, 0); + var localDate = new LocalDate(2024, 6, 15); + var duration = Duration.FromHours(2); + + Console.WriteLine($"NodaTime types: Instant={instant}, LocalDate={localDate}, Duration={duration}"); + } + + // Argon.Xml: Verify XmlNodeConverter is accessible + static void TestXmlTypesAccessible() + { + // Verify XmlNodeConverter can be instantiated + var converter = new XmlNodeConverter + { + DeserializeRootElementName = "root", + WriteArrayAttribute = false, + OmitRootObject = false + }; + + Console.WriteLine($"XmlConverter: DeserializeRootElementName={converter.DeserializeRootElementName}"); + + // Verify it can be added to settings + var settings = new JsonSerializerSettings(); + settings.Converters.Add(converter); + + if (settings.Converters.Count == 0) + { + throw new Exception("Expected XmlNodeConverter to be added"); + } + } +} diff --git a/src/Argon.DataSets/Argon.DataSets.csproj b/src/Argon.DataSets/Argon.DataSets.csproj index b6410767f..ba95b95d6 100644 --- a/src/Argon.DataSets/Argon.DataSets.csproj +++ b/src/Argon.DataSets/Argon.DataSets.csproj @@ -1,6 +1,7 @@ net472;net48;net6.0;net7.0;net8.0;net9.0;net10.0 + true diff --git a/src/Argon.FSharp/Argon.FSharp.csproj b/src/Argon.FSharp/Argon.FSharp.csproj index a20894516..e9733eb84 100644 --- a/src/Argon.FSharp/Argon.FSharp.csproj +++ b/src/Argon.FSharp/Argon.FSharp.csproj @@ -3,6 +3,7 @@ Provides serialization support between FSharp and Argon. net48;net6.0;net7.0;net8.0;net9.0;net10.0 + true diff --git a/src/Argon.InterfaceCallbacks/Argon.InterfaceCallbacks.csproj b/src/Argon.InterfaceCallbacks/Argon.InterfaceCallbacks.csproj index b6410767f..ba95b95d6 100644 --- a/src/Argon.InterfaceCallbacks/Argon.InterfaceCallbacks.csproj +++ b/src/Argon.InterfaceCallbacks/Argon.InterfaceCallbacks.csproj @@ -1,6 +1,7 @@ net472;net48;net6.0;net7.0;net8.0;net9.0;net10.0 + true diff --git a/src/Argon.JsonPath/Argon.JsonPath.csproj b/src/Argon.JsonPath/Argon.JsonPath.csproj index b6410767f..ba95b95d6 100644 --- a/src/Argon.JsonPath/Argon.JsonPath.csproj +++ b/src/Argon.JsonPath/Argon.JsonPath.csproj @@ -1,6 +1,7 @@ net472;net48;net6.0;net7.0;net8.0;net9.0;net10.0 + true diff --git a/src/Argon.NodaTime/Argon.NodaTime.csproj b/src/Argon.NodaTime/Argon.NodaTime.csproj index 388fc38f4..cd05b49ef 100644 --- a/src/Argon.NodaTime/Argon.NodaTime.csproj +++ b/src/Argon.NodaTime/Argon.NodaTime.csproj @@ -4,6 +4,7 @@ Provides serialization support between Noda Time and Json.NET. net48;net6.0;net7.0;net8.0;net9.0;net10.0 nodatime;json;Argon + true diff --git a/src/Argon.Xml/Argon.Xml.csproj b/src/Argon.Xml/Argon.Xml.csproj index b6410767f..ba95b95d6 100644 --- a/src/Argon.Xml/Argon.Xml.csproj +++ b/src/Argon.Xml/Argon.Xml.csproj @@ -1,6 +1,7 @@ net472;net48;net6.0;net7.0;net8.0;net9.0;net10.0 + true diff --git a/src/Argon.slnx b/src/Argon.slnx index e1bf9803d..0cce2b78e 100644 --- a/src/Argon.slnx +++ b/src/Argon.slnx @@ -20,4 +20,5 @@ + diff --git a/src/appveyor.yml b/src/appveyor.yml index a455d94fb..9bfde58b2 100644 --- a/src/appveyor.yml +++ b/src/appveyor.yml @@ -23,6 +23,7 @@ build_script: } - dotnet build src --configuration Release - dotnet test src --configuration Release --no-build --no-restore +- dotnet publish src/Argon.AotTests/Argon.AotTests.csproj --configuration Release on_failure: - ps: Get-ChildItem *.received.* -recurse | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } test: off