diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e3d81fa..865f76e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -44,6 +44,9 @@ jobs:
- name: Test Aot with coverage (net10)
run: dotnet test JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/aot
+ - name: Test Aot generator with coverage (net10)
+ run: dotnet test JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/generator
+
- name: Install gpg (required by codecov-action v5)
run: sudo apt-get install -y gpg
# codecov-action v5 verifies the Codecov binary signature and requires
@@ -57,6 +60,7 @@ jobs:
TestResults/newtonsoft/**/coverage.cobertura.xml
TestResults/textjson/**/coverage.cobertura.xml
TestResults/aot/**/coverage.cobertura.xml
+ TestResults/generator/**/coverage.cobertura.xml
fail_ci_if_error: true
# The Aot sample is the only top-level-statements consumer of the generator:
@@ -95,11 +99,15 @@ jobs:
run: dotnet test JsonSubTypes.Tests/JsonSubTypes.Tests.csproj -c Release -f net46
analysis:
- # SonarCloud runs on master only. The job-level `if` avoids referencing the
- # secret in a step-level `if`, which broke workflow startup before (secrets are
- # not allowed in `if:` conditions).
+ # SonarCloud analyzes both master pushes and pull requests. For PRs the scanner
+ # needs the pull-request parameters plus GITHUB_TOKEN (used to decorate the PR
+ # with the quality gate). The job-level `if` avoids referencing the secret in a
+ # step-level `if`, which broke workflow startup before (secrets are not allowed
+ # in `if:` conditions).
runs-on: ubuntu-latest
- if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- uses: actions/checkout@v5
with:
@@ -122,9 +130,17 @@ jobs:
# job builds and tests the solution itself rather than reusing modern's
# reports (whose relative file paths would not resolve here).
- name: Begin SonarCloud analysis
- run: dotnet-sonarscanner begin /k:manuc66_JsonSubTypes /o:manuc66-github /d:sonar.token="$SONAR_TOKEN" /d:sonar.host.url=https://sonarcloud.io /d:sonar.cs.cobertura.reportsPaths=TestResults/**/coverage.cobertura.xml /d:sonar.exclusions=**/*Tests*/**/*,**/JsonSubTypes.Benchmarks/**/*,**/JsonSubTypes.Aot.Sample/**/*
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ PR_KEY: ${{ github.event.pull_request.number }}
+ PR_BRANCH: ${{ github.head_ref }}
+ PR_BASE: ${{ github.base_ref }}
+ run: |
+ pr_args=""
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ pr_args="/d:sonar.pullrequest.key=$PR_KEY /d:sonar.pullrequest.branch=$PR_BRANCH /d:sonar.pullrequest.base=$PR_BASE"
+ fi
+ dotnet-sonarscanner begin /k:manuc66_JsonSubTypes /o:manuc66-github /d:sonar.token="$SONAR_TOKEN" /d:sonar.host.url=https://sonarcloud.io /d:sonar.cs.cobertura.reportsPaths=TestResults/**/coverage.cobertura.xml /d:sonar.exclusions=**/*Tests*/**/*,**/JsonSubTypes.Benchmarks/**/*,**/JsonSubTypes.Aot.Sample/**/* /d:sonar.cpd.exclusions=**/*.g.cs $pr_args
- name: Build for SonarCloud analysis
run: dotnet build JsonSubTypes.sln -c Release
@@ -134,8 +150,10 @@ jobs:
dotnet test JsonSubTypes.Tests/JsonSubTypes.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/newtonsoft
dotnet test JsonSubTypes.Text.Json.Tests/JsonSubTypes.Text.Json.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/textjson
dotnet test JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/aot
+ dotnet test JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/generator
- name: End SonarCloud analysis
run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN"
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/JsonSubTypes.Aot.Generated/Generated/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/Generated/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs
new file mode 100644
index 0000000..e99e608
--- /dev/null
+++ b/JsonSubTypes.Aot.Generated/Generated/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs
@@ -0,0 +1,309 @@
+//
+#nullable enable
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace JsonSubTypes.Aot.Generated
+{
+ /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Animal.
+ [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")]
+ public sealed class AnimalJsonSubTypesConverter : JsonConverter
+ {
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal);
+
+ public override void Write(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options)
+ {
+ if (value is null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+ Type runtimeType = value.GetType();
+ string payload;
+ if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal))
+ {
+ if (IsRegistered(runtimeType))
+ {
+ payload = SerializeBasePayload((global::JsonSubTypes.Aot.Generated.TestDomain.Animal)value, options);
+ }
+ else
+ {
+ WriteBaseObject(writer, (global::JsonSubTypes.Aot.Generated.TestDomain.Animal)value, options);
+ return;
+ }
+ }
+ else if (IsRegistered(runtimeType))
+ {
+ payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType));
+ }
+ else if (TryWriteNestedObject(writer, value, runtimeType, options))
+ {
+ return;
+ }
+ else if (TryWriteDynamic(writer, value, runtimeType, options))
+ {
+ return;
+ }
+ else
+ {
+ JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType));
+ return;
+ }
+ using JsonDocument payloadDocument = JsonDocument.Parse(payload);
+ string discriminatorName = "type";
+ if (options.PropertyNamingPolicy != null)
+ {
+ discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName);
+ }
+ writer.WriteStartObject();
+ writer.WritePropertyName(discriminatorName);
+ WriteDiscriminatorValue(writer, runtimeType, options);
+ foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject())
+ {
+ if (!property.NameEquals(discriminatorName))
+ {
+ property.WriteTo(writer);
+ }
+ }
+ writer.WriteEndObject();
+ }
+
+ public override global::JsonSubTypes.Aot.Generated.TestDomain.Animal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException("Unrecognized token: " + reader.TokenType);
+ }
+ using JsonDocument document = JsonDocument.ParseValue(ref reader);
+ JsonElement root = document.RootElement;
+ Type target = SelectType(root, options);
+ if (target == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal))
+ {
+ return DeserializeBase(root, options);
+ }
+ return (global::JsonSubTypes.Aot.Generated.TestDomain.Animal?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target));
+ }
+
+ private Type SelectType(JsonElement root, JsonSerializerOptions options)
+ {
+ if (TryGetValueInJson(root, "type", options, out JsonElement discriminator))
+ {
+
+ if (discriminator.ValueKind == JsonValueKind.String)
+ {
+ switch (discriminator.GetString())
+ {
+ case "cat": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Cat);
+ }
+ }
+ if (discriminator.ValueKind == JsonValueKind.Number)
+ {
+ switch (discriminator.GetRawText())
+ {
+ case "2": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Dog);
+ }
+ }
+ if (TryGetDynamicType(discriminator, out Type? dynamicType))
+ {
+ return dynamicType!;
+ }
+ if (CustomTypeNameResolver is not null)
+ {
+ Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator));
+ if (customType != null)
+ {
+ return customType;
+ }
+ }
+ return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal);
+ }
+ return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal);
+ }
+
+ private static readonly System.Collections.Generic.Dictionary> DiscriminatorWriters = new System.Collections.Generic.Dictionary>
+ {
+ [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Cat)] = static (writer, options) => writer.WriteStringValue("cat"),
+ [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Dog)] = static (writer, options) => writer.WriteNumberValue(2)
+ };
+
+ private static bool IsRegistered(Type runtimeType)
+ {
+ return DiscriminatorWriters.ContainsKey(runtimeType);
+ }
+
+ private static void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options)
+ {
+ if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write))
+ {
+ write(writer, options);
+ return;
+ }
+ throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property");
+ }
+
+ public readonly System.Collections.Concurrent.ConcurrentDictionary