Fix NativeAOT round-trip for tm7 file I/O#3
Merged
Conversation
The published AOT exe could neither read nor write .tm7 files: the reflection-based DataContractSerializer hit three AOT-incompatible patterns (Dictionary<,> falling back to ISerializable, Nullable<T> member serialization via MakeGenericMethod, and EmitDefaultValue=false calling GetDefaultValue<T>()). Model refactor: - Add SerializableKvpTypes.cs with three [DataContract] KVP types plus matching [CollectionDataContract] List wrappers; element names match what DCS auto-generates (KeyValueOfguidanyType, KeyValueOfstringThreat- pc_P0_PhOB, KeyValueOfstringstring) so wire compat with TMT is preserved. - Mark Borders / Lines / AllThreatsDictionary / SerializableThreat.Properties [IgnoreDataMember] and back each with a private [DataMember] List<Kvp> field synced via [OnSerializing] / [OnDeserialized] callbacks. Public Dictionary<,> API surface unchanged - no consumer touched. - Change bool? ThreatGenerationEnabled to bool (Nullable<T> reflection- writer path is incompatible with AOT). Constructor still accepts bool? and coerces null to false. - Drop EmitDefaultValue=false from Zoom / StrokeThickness / StrokeDashArray (DCS's GetDefaultValue<T> uses MakeGenericMethod which AOT cannot generate). Wire bytes now include these fields with default/null values. Build / serializer plumbing: - Switch IL3050/IL3053 from NoWarn to WarningsNotAsErrors so the BCL DCS warnings remain visible without failing the build. - Drop the dead string-typed DynamicDependency entries for KeyValue<,> closed generics (a failed experimental approach) and the analogous trimmer-root entry. Keep Dictionary<,> / EqualityComparer<,> preservation as defensive in case a future regression re-introduces a [DataMember] Dictionary<,>. Tests / CI: - Replace the byte-for-byte round-trip test (no longer achievable) with a structural-equivalence test. - Add PopulatedDictionaryTests covering populated AllThreatsDictionary, populated SerializableThreat.Properties, multiple add/remove cycles, null-dictionary serialization, and a wire-format guard that compares our KVP element names against DCS-generated names for the equivalent raw Dictionary<,>. The wire-format test caught a wrong-name guess during development. - Add AotExeIntegrationTests that shell out to the published AOT exe and validate populated-threat round-trip through the reflection writer (the JIT in-proc tests use code-gen and cannot exercise that path). Self-skip when the publish output is absent. - Add a Publish AOT step to the CI workflow ahead of Test so the integration tests actually run on every matrix leg. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The published NativeAOT exe could neither read nor write .tm7 files. The reflection-based DataContractSerializer hits three AOT-incompatible patterns under .NET 10:
GetDefaultValue1via MakeGenericMethod, which is fundamentally AOT-incompatible.double/stringmembers — same MakeGenericMethod path throughXmlObjectSerializerWriteContext.GetDefaultValue1.Fix
Model refactor
SerializableKvpTypes.cswith three[DataContract]KVP types + matching[CollectionDataContract]List wrappers. Element names match what DCS auto-generates (KeyValueOfguidanyType,KeyValueOfstringThreatpc_P0_PhOB,KeyValueOfstringstring) so wire compatibility with TMT is preserved.Borders/Lines/AllThreatsDictionary/SerializableThreat.Propertiesmarked[IgnoreDataMember]and each backed by a private[DataMember]list field synced via[OnSerializing]/[OnDeserialized]callbacks. The publicDictionary<,>API surface is unchanged — no consumer touched.bool? ThreatGenerationEnabled→bool(constructor still acceptsbool?; coerces null → false).EmitDefaultValue=falsefromZoom/StrokeThickness/StrokeDashArray.Build / serializer plumbing
IL3050;IL3053fromNoWarntoWarningsNotAsErrorsso BCL DCS warnings stay visible without failing the build.DynamicDependencyentries forKeyValue<,>closed generics (failed approach). KeepDictionary<,>/EqualityComparer<,>preservation as defensive against future[DataMember] Dictionary<,>regressions.Tests + CI
RoundTrip_StructuralEquivalenceRoundtrip_PopulatedThreatInstances_PreservesEntriesAllThreatsDictionary, verifies full state round-trip.Roundtrip_PopulatedThreatProperties_PreservesEntriesDictionary<string,string>including empty-string edge case.Roundtrip_PopulatedThreats_WireFormatMatchesAutoDictionaryDictionary<,>. Caught a wrong-name guess during development.Roundtrip_PopulatedThreatProperties_WireFormatMatchesAutoDictionaryDictionary<string,string>.Roundtrip_Borders_WireFormatMatchesAutoDictionaryDictionary<Guid,object>.Roundtrip_MultipleAddRemoveCyclesSerialize_NullDictionaries_DoesNotThrowAotExe_Open_LoadsTemplateAotExe_AddEntity_RoundTripsAndPreservesExistingEntitiesAotExe_PopulatedThreatsRoundTripThroughAddCommandAdded a
Publish AOTstep to.github/workflows/ci.ymlahead ofTestso the integration tests actually run on every matrix leg (ubuntu / windows / macos). Tests useRuntimeInformation.RuntimeIdentifierto locate the platform-correct exe.Total: 17 → 20 tests, all passing.
Caveats
.tm7opens in the actual TMT app — the wire format diverges slightly becauseEmitDefaultValue=falseis gone andThreatGenerationEnabledis no longer nullable. DCS readers are tolerant; TMT-specific parsing isn't proven.EmitDefaultValue=falseremoval cannot be worked around without AOT supportingMakeGenericMethodon BCL-internal DCS methods.Verification
dotnet test.tm7.exe open / new / add entityall succeed againstsamples/template.tm7using the published AOT binary.