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
28 changes: 23 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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 }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
// <auto-generated/>
#nullable enable
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace JsonSubTypes.Aot.Generated
{
/// <summary>Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Animal.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")]
public sealed class AnimalJsonSubTypesConverter : JsonConverter<global::JsonSubTypes.Aot.Generated.TestDomain.Animal>
{
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<System.Type, System.Action<Utf8JsonWriter, JsonSerializerOptions>> DiscriminatorWriters = new System.Collections.Generic.Dictionary<System.Type, System.Action<Utf8JsonWriter, JsonSerializerOptions>>
{
[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<Utf8JsonWriter, JsonSerializerOptions>? 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<object, Type> DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary<object, Type>();
private readonly System.Collections.Concurrent.ConcurrentDictionary<Type, object> _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary<Type, object>();

public void RegisterDynamicSubtype(object discriminator, Type type)
{
DynamicSubtypes[discriminator] = type;
_dynamicReverse[type] = discriminator; // last registration wins, like the builder
}

/// <summary>
/// Custom discriminator-to-type resolution hook, invoked after the static
/// registrations and DynamicSubtypes. Assign it to implement your own
/// name-based lookup (e.g. assembly scanning, a DI registry). The resolved
/// type must be resolvable by the TypeInfoResolver (in the source-gen context
/// for Native AOT).
/// </summary>
public Func<object?, Type?>? CustomTypeNameResolver { get; set; }

private static object? GetDiscriminatorKey(JsonElement discriminator)
{
switch (discriminator.ValueKind)
{
case JsonValueKind.String:
return discriminator.GetString();
case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt):
return keyInt;
default:
return discriminator.GetRawText();
}
}

private bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType)
{
switch (discriminator.ValueKind)
{
case JsonValueKind.String:
return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType);
case JsonValueKind.Number:
if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType))
{
return true;
}
return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType);
default:
return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType);
}
}

private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options)
{

return false;
}

private bool TryWriteDynamic(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options)
{
if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator))
{
writer.WriteStartObject();
string dynamicDiscriminatorName = "type";
if (options.PropertyNamingPolicy != null)
{
dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName);
}
writer.WritePropertyName(dynamicDiscriminatorName);
writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType())));
string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType));
using JsonDocument payloadDocument = JsonDocument.Parse(payload);
foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject())
{
property.WriteTo(writer);
}
writer.WriteEndObject();
return true;
}
return false;
}
private static string SerializeBasePayload(global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options)
{
using System.IO.MemoryStream stream = new System.IO.MemoryStream();
using (Utf8JsonWriter writer = new Utf8JsonWriter(stream))
{
WriteBaseObject(writer, value, options);
}
return System.Text.Encoding.UTF8.GetString(stream.ToArray());
}

private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options)
{
writer.WriteStartObject();
string nameAge = "Age";
if (options.PropertyNamingPolicy != null && true)
{
nameAge = options.PropertyNamingPolicy.ConvertName(nameAge);
}
writer.WritePropertyName(nameAge);
JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int)));
writer.WriteEndObject();
}

private static global::JsonSubTypes.Aot.Generated.TestDomain.Animal DeserializeBase(JsonElement root, JsonSerializerOptions options)
{
global::JsonSubTypes.Aot.Generated.TestDomain.Animal instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Animal();
if (TryGetProperty(root, "Age", options, out JsonElement AgeValue))
{
instance.Age = (int)JsonSerializer.Deserialize(AgeValue.GetRawText(), options.GetTypeInfo(typeof(int)))!;
}
return instance;
}

private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value)
{
if (root.TryGetProperty(name, out value))
{
return true;
}
string? convertedName = options.PropertyNamingPolicy?.ConvertName(name);
if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value))
{
return true;
}
if (options.PropertyNameCaseInsensitive)
{
foreach (JsonProperty property in root.EnumerateObject())
{
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
{
value = property.Value;
return true;
}
}
}
return false;
}

private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value)
{
if (TryGetProperty(root, propertyName, options, out value))
{
return true;
}
if (propertyName.IndexOf('.') >= 0)
{
string[] segments = propertyName.Split('.');
JsonElement current = root;
foreach (string segment in segments)
{
if (!TryGetProperty(current, segment, options, out current))
{
value = default;
return false;
}
}
value = current;
return true;
}
return false;
}
}
}
Loading
Loading