Skip to content

Improve Native AOT Support (Closes #1085)#1092

Merged
EdwardCooke merged 11 commits into
aaubry:masterfrom
fdcastel:issue-1085
Apr 9, 2026
Merged

Improve Native AOT Support (Closes #1085)#1092
EdwardCooke merged 11 commits into
aaubry:masterfrom
fdcastel:issue-1085

Conversation

@fdcastel

@fdcastel fdcastel commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Based on #1089. Closes #1085.

This PR implements four of the seven suggestions from #1085 to improve YamlDotNet's Native AOT support via StaticDeserializerBuilder and the YamlDotNet.Analyzers.StaticGenerator source generator.

Changes

1. Better Error Messages for Unregistered Types (Suggestion #5)

Files changed: StaticObjectFactoryFile.cs, ObjectTests.cs

Replaced ArgumentOutOfRangeException with InvalidOperationException in the generated StaticObjectFactory code. The new error message clearly says:

Type 'MyNamespace.MyType' is not registered in the YamlDotNet static context. Add [YamlSerializable(typeof(MyType))] to your static context class.

This replaces the ambiguous "Unknown type: ..." message thrown as ArgumentOutOfRangeException, which was misleading and hard to diagnose.

2. Support required Members in Generated Object Factories (Suggestion #2)

Files changed: StaticObjectFactoryFile.cs, ObjectTests.cs

The source generator now detects C# required properties and fields (C# 11+) and emits object initializer syntax with default! values instead of bare new T(). This prevents CS9035 compile errors when types have required members.

Before (fails):

if (type == typeof(MyType)) return new MyType();

After (works):

if (type == typeof(MyType)) return new MyType() { Name = default!, Value = default! };

3. Support OrderedDictionary<TKey, TValue> in the Static Context (Suggestion #3)

Files changed: SerializableSyntaxReceiver.cs

Added recognition of System.Collections.Generic.OrderedDictionary<TKey, TValue> (.NET 9+) in the source generator's CheckForSupportedGeneric method. The type is now correctly identified as a dictionary, enabling proper code generation for IsDictionary, GetKeyType, GetValueType, and Create methods.

4. Built-in Type Converters for TimeSpan and Uri (Suggestion #4)

Files changed: TimeSpanConverter.cs (new), UriConverter.cs (new), BuilderSkeleton.cs, StaticBuilderSkeleton.cs, SerializerBuilder.cs, StaticSerializerBuilder.cs

Added TimeSpanConverter and UriConverter as built-in IYamlTypeConverter implementations, registered by default in both the regular and static builder skeletons. Both converters support JSON-compatible mode (double-quoted output).

Skipped Suggestions

The following suggestions were intentionally deferred:

  • 1. Publish Source Generator NuGet Package: Packaging/release concern, not a code change.
  • 6. Roslyn Analyzer for Missing Type Registrations: High-effort analyzer work requiring separate design.
  • 7. Document StaticDeserializerBuilder API Parity Gaps: Documentation-only task.

Testing

  • All existing tests pass (28 static generator tests on net8.0, 27 on net6.0).
  • New tests added:
    • UnregisteredTypeThrowsDescriptiveException — verifies the improved error message
    • RequiredMembersWork — verifies required property round-trip (net8.0 only)
    • TimeSpanConverterTests (9 tests) — unit + round-trip tests
    • UriConverterTests (8 tests) — unit + round-trip tests
  • Full solution builds successfully across all target frameworks (net8.0, net6.0, net47, netstandard2.0, netstandard2.1).

Pre-existing Test Failures

4 tests in UnquotedStringTypeDeserialization_RegularNumbers fail due to floating-point precision/locale issues unrelated to this PR. These failures also occur on master.

Add net9.0 and net10.0 to both the library and test project TargetFrameworks.

Fix OrderedDictionary ambiguity for net9.0+: .NET 9 introduced
System.Collections.Generic.OrderedDictionary<TKey, TValue> which conflicts
with YamlDotNet.Helpers.OrderedDictionary<TKey, TValue>. Disambiguate by
using fully qualified type names in YamlMappingNode.cs and
OrderedDictionaryTests.cs.

Update appveyor.yml to install the .NET 10 SDK and add artifact paths for
the new target frameworks.
@fdcastel
fdcastel marked this pull request as draft March 26, 2026 20:51
@fdcastel

Copy link
Copy Markdown
Contributor Author

Added: Nullable type support for all built-in type converters

The latest commit (f6eba0e) extends all built-in converters to handle nullable types.

Problem

The converters introduced in this PR (TimeSpanConverter, UriConverter) and other pre-existing ones (GuidConverter, DateTimeConverter, DateTime8601Converter, DateTimeOffsetConverter, DateOnlyConverter, TimeOnlyConverter) only handled the base type — not the nullable counterpart. For example, TimeSpanConverter.Accepts(typeof(TimeSpan?)) returned false, so TimeSpan? properties were not handled by the converter at all.

Changes

All value-type converters (Guid, TimeSpan, DateTime, DateTimeOffset, DateOnly, TimeOnly):

Reference-type converter (Uri):

  • ReadYaml() and WriteYaml() now handle null values (same pattern — Accepts() didn't need changes since Uri is already nullable by nature)

SystemTypeConverter is intentionally excluded — already fixed by PR #1091.

Tests

Added NullableTypeConverterTests.cs with 18 tests covering:

  • Accepts() returns true for each Nullable<T> type
  • Round-trip (serialize → deserialize) for nullable properties with a value
  • Round-trip for nullable properties with null

All existing tests continue to pass.

@fdcastel

fdcastel commented Mar 28, 2026

Copy link
Copy Markdown
Contributor Author

Updated PR — New features added

This PR is now rebased on top of feature/add-net9-net10-targets (#1089) to take advantage of the .NET 9/10 target framework support.

Summary of changes

1. Improve error messages for unregistered types in static context

Clear, actionable error messages when a type is missing from the static context, instead of cryptic ArgumentOutOfRangeException.

2. Support required members in generated object factories

The source generator now correctly handles C# 11 required properties/fields in [YamlSerializable] types.

3. Support OrderedDictionary<TKey, TValue> in the static context

The static generator recognizes OrderedDictionary<TKey, TValue> (introduced in .NET 9) as a dictionary type, generating correct factory, accessor, and type inspector code.

4. Built-in type converters for TimeSpan and Uri

New TimeSpanConverter and UriConverter that handle serialization/deserialization with proper formatting control, similar to the existing DateTimeConverter.

5. Handle nullable types in all built-in type converters

All built-in converters (DateTimeConverter, DateTimeOffsetConverter, GuidConverter, TimeSpanConverter, UriConverter, DateOnlyConverter, TimeOnlyConverter) now correctly accept and unwrap Nullable<T> during deserialization instead of throwing.

6. IParsable<T> support in ScalarNodeDeserializer

Types with a static Parse(string, IFormatProvider) method — including all IParsable<T> implementations (.NET 7+) — are now automatically deserialized from YAML scalars without needing a custom IYamlTypeConverter. This is especially important for the static/AOT deserialization path where the NullTypeConverter cannot perform type conversions.

7. Roslyn diagnostic YDNG001 for unregistered types

The source generator now emits a compile-time warning when a property or field on a [YamlSerializable] type references a type that is not registered in the static context. The diagnostic checks types recursively, handles nullable types, skips well-known BCL primitives, and walks generic collection type arguments.

Test results

All tests pass on net8.0, net9.0, and net10.0 (1890 passing; the 4 locale-dependent UnquotedStringTypeDeserialization_RegularNumbers failures are pre-existing on master).

@fdcastel
fdcastel marked this pull request as ready for review March 28, 2026 01:34
Replace ArgumentOutOfRangeException with InvalidOperationException in the
generated StaticObjectFactory code. The new error message clearly indicates
that the type is not registered and suggests adding [YamlSerializable] to
the static context class.

Addresses suggestion aaubry#5 from YAML_DOTNET_SUGGESTIONS.md.
The source generator now detects C# 'required' properties and fields and
emits object initializer syntax with 'default!' values instead of bare
'new T()'. This prevents CS9035 compile errors when types have required
members.

Generated code example:
  new MyType() { RequiredProp = default!, RequiredField = default! }

Addresses suggestion aaubry#2 from YAML_DOTNET_SUGGESTIONS.md.
Add recognition of System.Collections.Generic.OrderedDictionary<TKey, TValue>
(.NET 9+) in the source generator's CheckForSupportedGeneric method. The type
is now correctly identified as a dictionary, enabling proper code generation
for IsDictionary, GetKeyType, GetValueType, and Create methods.

Addresses suggestion aaubry#3 from YAML_DOTNET_SUGGESTIONS.md.
Add TimeSpanConverter and UriConverter as built-in IYamlTypeConverter
implementations, registered by default in both the regular and static
builder skeletons. This means TimeSpan and Uri values now round-trip
correctly without requiring users to write custom converters.

Both converters support JSON-compatible mode (double-quoted output) and
are automatically swapped in when JsonCompatible() is called on the
serializer builder.

Addresses suggestion aaubry#4 from YAML_DOTNET_SUGGESTIONS.md.
All value-type converters (Guid, TimeSpan, DateTime, DateTimeOffset,
DateOnly, TimeOnly) now accept their Nullable<T> counterpart.

All converters (including reference-type Uri) now handle null values
in ReadYaml (return null for empty/null scalars) and WriteYaml (emit
an empty scalar instead of throwing NullReferenceException).

SystemTypeConverter is intentionally excluded — it is already fixed
by PR aaubry#1091.
Types with a static Parse(string, IFormatProvider) method — such as
those implementing IParsable<T> (.NET 7+) — are now automatically
deserialized from YAML scalars without needing a custom IYamlTypeConverter.

This is especially important for the static/AOT deserialization path
where the NullTypeConverter cannot perform type conversions. Types like
TimeSpan, DateTimeOffset, DateOnly, TimeOnly, Guid, and IPAddress now
work out of the box in both the regular and static deserializer paths.
The source generator now emits a compile-time warning (YDNG001) when a
property or field on a [YamlSerializable] type references a type that
is not itself registered in the YamlDotNet static context.

This catches missing [YamlSerializable(typeof(T))] registrations at
compile time instead of at runtime, preventing ArgumentOutOfRangeException
errors during deserialization.

The diagnostic checks property/field types recursively, handles nullable
types, and skips well-known BCL types (primitives, Guid, TimeSpan, etc.)
and generic collections (verifying their element types instead).
@fdcastel
fdcastel marked this pull request as draft March 28, 2026 01:51
@fdcastel
fdcastel marked this pull request as ready for review March 28, 2026 02:00
@fdcastel

fdcastel commented Mar 28, 2026

Copy link
Copy Markdown
Contributor Author

I’m keeping all the changes in this PR for now, but feel free to suggest any modifications or ask me to split them into smaller PRs.

@fdcastel fdcastel changed the title Initial suggestions from #1085 Improving Native AOT Support (Closes #1085) Mar 28, 2026
@fdcastel fdcastel changed the title Improving Native AOT Support (Closes #1085) Improve Native AOT Support (Closes #1085) Mar 28, 2026
@EdwardCooke

Copy link
Copy Markdown
Collaborator

I merged in your other branches and now this one has conflicts. If you can get to them quickly I can merge them in in a few hours. Otherwise I'll probably create a branch from this PR and fix them in my repository and close this PR

@EdwardCooke

Copy link
Copy Markdown
Collaborator

Oh yeah, if you can remove net9.0 and net6.0 since those frameworks are EOL now that would be great.

@fdcastel

fdcastel commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

I merged in your other branches and now this one has conflicts. If you can get to them quickly I can merge them in in a few hours. Otherwise I'll probably create a branch from this PR and fix them in my repository and close this PR

Working on it... 👍🏻

# Conflicts:
#	YamlDotNet/Serialization/Converters/DateOnlyConverter.cs
#	YamlDotNet/Serialization/Converters/DateTime8601Converter.cs
#	YamlDotNet/Serialization/Converters/DateTimeConverter.cs
#	YamlDotNet/Serialization/Converters/DateTimeOffsetConverter.cs
#	YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs
@fdcastel

fdcastel commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

Merged latest master into this branch to resolve conflicts.

Conflict Summary

5 files had conflicts, all in YamlDotNet/Serialization/Converters/ — caused by the ScalarConverterBase<T> extraction from PR #1090 overlapping with the nullable type handling added in this PR.

Conflicting files:

  • DateOnlyConverter.cs
  • DateTime8601Converter.cs
  • DateTimeConverter.cs
  • DateTimeOffsetConverter.cs
  • TimeOnlyConverter.cs

Resolution

Instead of keeping per-converter Accepts() overrides for nullable support, I moved the nullable type check into ScalarConverterBase<T>.Accepts():

public bool Accepts(Type type)
{
    return type == typeof(T) || Nullable.GetUnderlyingType(type) == typeof(T);
}

This gives all converters inheriting from ScalarConverterBase<T> automatic nullable type support without method hiding.

For ReadYaml and WriteYaml, the resolution combines both sides:

  • Uses ConsumeScalarValue(parser) helper from master's refactoring
  • Keeps the null/empty value checks needed for nullable type support
  • Keeps the null guard in WriteYaml (emitting empty scalar for null values)

Verification

  • Build succeeds across all 7 target frameworks (0 warnings, 0 errors)
  • All 201 converter tests pass
  • All 31 static generator tests pass

@fdcastel

fdcastel commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

Oh yeah, if you can remove net9.0 and net6.0 since those frameworks are EOL now that would be great.

Doing...

@fdcastel

fdcastel commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

Done — removed net9.0 and net6.0 from all projects (commit 45f94da).

Files updated:

  • YamlDotNet/YamlDotNet.csproj: net10.0;net8.0;netstandard2.0;netstandard2.1;net47
  • YamlDotNet.Test/YamlDotNet.Test.csproj: net10.0;net8.0;net47
  • YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj: net8.0;net47
  • appveyor.yml: removed Release-Net60 and Release-Net90 artifact entries
  • Dockerfile / Dockerfile.NonEnglish: removed net6.0 build and test steps

All 1972 tests pass on net8.0.

mkaraki pushed a commit to mkaraki/Diffcord that referenced this pull request Jul 21, 2026
Updated [Discord.Net](https://github.com/discord-net/Discord.Net) from
3.19.1 to 3.20.1.

<details>
<summary>Release notes</summary>

_Sourced from [Discord.Net's
releases](https://github.com/discord-net/Discord.Net/releases)._

## 3.20.1

## [3.20.1] - 2026-06-07
This release fixes a regression introduced in 3.20.0

### Fixed
- #​3276 Handle null VoiceChannel in SocketVoiceState constructor
(61ed916)

**Full Changelog**:
discord-net/Discord.Net@3.20.0...3.20.1

## 3.20.0

## [3.20.0] - 2026-06-06
This release brings support for checkboxes and checkbox/radio groups in
modals, and also covers the "new" message search endpoint.

### Breaking changes
- `SelectMenuOptionAttribute` from the Interaction Framework was renamed
to `EnumOptionAttribute`.

### Added
- #​3232 IF modal radio buttons, and checkboxes (c95fbf6)
- #​3268 add support for getting messages from a guild (with filters)
(31fed25)
- #​3255 add missing audit log action types (4476eea)
- #​3265 Add GET voice-state REST wrappers (13d83da)

### Fixed
- #​3258 propagate parent module attributes to child commands (cbc61d9)
- #​3263 strip RTP padding before DAVE decrypt (RFC 3550 В§5.1)
(1a843fb)
- #​3256 Add empty payload check (6527e71)
- #​3264 Fix reference to PreCompiledLambdas/UseCompiledLambda (763aa79)
- #​3271 fix for #​3269 (9abfbfd)
- #​3272 Fix default array converter in modals & add docs for
checkboxes/radio groups (527764c)

### Misc
- #​3254 user `global_name` description (05af64b)
- #​3257 feat(Core): add missing JSON error codes (4272ae1)
- #​3259 refactor(Core): rename JSON error code (504e1db)
- #​3261 Message call data timestamp nullability (5a328a0)
- #​3266 Add play audio sample (4d8b0bc)

## New Contributors
* @​Archivelit made their first contribution in
discord-net/Discord.Net#3256
* @​Sim-hu made their first contribution in
discord-net/Discord.Net#3255
* @​yury-opolev made their first contribution in
discord-net/Discord.Net#3263
* @​apartje made their first contribution in
discord-net/Discord.Net#3271

**Full Changelog**:
discord-net/Discord.Net@3.19.1...3.20.0

Commits viewable in [compare
view](discord-net/Discord.Net@3.19.1...3.20.1).
</details>

Updated [DotNetEnv](https://github.com/tonerdo/dotnet-env) from 3.1.1 to
3.2.0.

<details>
<summary>Release notes</summary>

_Sourced from [DotNetEnv's
releases](https://github.com/tonerdo/dotnet-env/releases)._

## 3.2.0

- Switch parsing to Superpower (from Sprache)
- Fix utf8 parsing
- Interpolated variables parsing


Commits viewable in [compare
view](tonerdo/dotnet-env@v3.1.1...v3.2.0).
</details>

Updated [YamlDotNet](https://github.com/aaubry/YamlDotNet) from 16.3.0
to 18.1.0.

<details>
<summary>Release notes</summary>

_Sourced from [YamlDotNet's
releases](https://github.com/aaubry/YamlDotNet/releases)._

## 18.1.0

## What's Changed
* Use NET 10 with benchmarks by @​mcraiha in
aaubry/YamlDotNet#1099
* Revert package upgrades by @​EdwardCooke in
aaubry/YamlDotNet#1104
* Added default maximum recursion level of 130 (max when using defaults
on Windows/.net8) by @​EdwardCooke in
aaubry/YamlDotNet#1110
* Static deserializer builder needed the default maximum recursion by
@​EdwardCooke in aaubry/YamlDotNet#1111

## New Contributors
* @​mcraiha made their first contribution in
aaubry/YamlDotNet#1099

**Full Changelog**:
aaubry/YamlDotNet@v18.0.0...v18.1.0

## Breaking
* Maximum depth of yaml files is now 130 by default. If you need higher
you will need to adjust the maximum yaml depth. Going above 130 runs the
risk of stack overflow exceptions when any exception happens inside of
the deserialization

## 18.0.0

## What's Changed
* Add a parse method wrapper and caching to fix AoT compilation by
@​EdwardCooke in aaubry/YamlDotNet#1103
**BREAKING CHANGE** This is a breaking change in the
`TypeInspectorSkeleton` class and the `ITypeInspector` interface by
adding 2 methods . Quick fix to resolve those breaking changes in your
own custom TypeInspector is to return false on the HasParseMethod method
and return null or throw an exception on the Parse method.


**Full Changelog**:
aaubry/YamlDotNet@v17.1.0...v18.0.0

## 17.1.0

## What's Changed
* Security improvements by @​EdwardCooke in
aaubry/YamlDotNet#1102
There was a potential breaking change for large yaml files in the
MergingParser. You may need to specify the optional parameter for
maximum events to be processed. It default to 100k events which is a
very large yaml file.


**Full Changelog**:
aaubry/YamlDotNet@v17.0.0...v17.1.0

## 17.0.0

## What's Changed
* Clean-up the "IsKey" logic by @​aaubry in
aaubry/YamlDotNet#1073
* Fix for gitversion and pinning it so it doesnt break...again. by
@​EdwardCooke in aaubry/YamlDotNet#1074
* Add max depth handling to StaticDeserializerBuilder (builds on #​1072)
by @​skdishansachin in aaubry/YamlDotNet#1082
* Allow specifying a maximum recursion for the deserializer by @​aaubry
in aaubry/YamlDotNet#1072
* Fix NullReferenceException when serializing null System.Type
properties by @​fdcastel in
aaubry/YamlDotNet#1091
* Reduce code duplication in converters and event emitters by @​fdcastel
in aaubry/YamlDotNet#1090
* Use pre-compiled static Regex instances in ScalarNodeDeserializer by
@​fdcastel in aaubry/YamlDotNet#1088
* Fix infinite loop in source generator exception handler by @​fdcastel
in aaubry/YamlDotNet#1087
* Fix TODOs, typos, and add missing tests by @​fdcastel in
aaubry/YamlDotNet#1086
* Fix YamlException.ToString() to include stack trace by
@​skdishansachin in aaubry/YamlDotNet#1084
* Fix remaining spec cases during parsing: L383, C2SP by @​am11 in
aaubry/YamlDotNet#1081
* Improve type fidelity in UnquotedStringTypeDeserialization test by
@​jhgbrt in aaubry/YamlDotNet#1076
* CodeQL Advanced Workflow by @​aluty in
aaubry/YamlDotNet#1067
* Nullable fixes in non-public code by @​Kielek in
aaubry/YamlDotNet#1064
* Use string interning by @​simonthum in
aaubry/YamlDotNet#1055
* Fix grammar in comments in DefaultValuesHandling.cs by @​209jkjkjk in
aaubry/YamlDotNet#1041
* fix #​1031 by @​dogdie233 in
aaubry/YamlDotNet#1033
* Improve Native AOT Support (Closes #​1085) by @​fdcastel in
aaubry/YamlDotNet#1092

## New Contributors
* @​skdishansachin made their first contribution in
aaubry/YamlDotNet#1082
* @​fdcastel made their first contribution in
aaubry/YamlDotNet#1091
* @​jhgbrt made their first contribution in
aaubry/YamlDotNet#1076
* @​aluty made their first contribution in
aaubry/YamlDotNet#1067
* @​Kielek made their first contribution in
aaubry/YamlDotNet#1064
* @​simonthum made their first contribution in
aaubry/YamlDotNet#1055
* @​209jkjkjk made their first contribution in
aaubry/YamlDotNet#1041
* @​dogdie233 made their first contribution in
aaubry/YamlDotNet#1033

**Full Changelog**:
aaubry/YamlDotNet@v16.3.0...v17.0.0

Commits viewable in [compare
view](aaubry/YamlDotNet@v16.3.0...v18.1.0).
</details>

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Suggestions for Improving Native AOT Support

2 participants