diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index ac05ebb181..a9f0614589 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -109,9 +109,9 @@ - - - + + + diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs index 57b4f9f19a..7068c7fa63 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using FluentAssertions.Execution; @@ -74,7 +75,24 @@ private IEnumerable GetPolymorphicTypesFor(Type type) [Trait("Category", "Verified")] public async Task VerifyChangeModels() { - await Verify(GetPolymorphicTypesFor(typeof(IChange))); + await Verify(GetChangePolymorphicTypes()); + } + + // IChange polymorphism is no longer expressed through PolymorphismOptions; each concrete change + // type instead carries a synthetic "$type" property (read by Harmony's change converter), so read + // the discriminator from there to snapshot the registered change set. + private IEnumerable GetChangePolymorphicTypes() + { + return LcmCrdtKernel.AllChangeTypes() + .Select(changeType => + { + var discriminatorProperty = _jsonSerializerOptions.GetTypeInfo(changeType).Properties + .SingleOrDefault(p => p.Name == CrdtConstants.ChangeDiscriminatorProperty); + discriminatorProperty.Should().NotBeNull("change type {0} should declare a $type discriminator", changeType); + var discriminator = discriminatorProperty!.Get!(RuntimeHelpers.GetUninitializedObject(changeType)); + return new JsonDerivedType(changeType, (string)discriminator!); + }) + .OrderBy(t => t.DerivedType.FullName); } [Fact] diff --git a/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs b/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs index 76bc8ad5a3..8f7191be71 100644 --- a/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs +++ b/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs @@ -18,6 +18,7 @@ public static JsonSerializerOptions Harmony() { var config = new CrdtConfig(); LcmCrdtKernel.ConfigureCrdt(config); + LcmCrdtKernel.FinalizeJsonSerializerOptions(config); return config.JsonSerializerOptions; } } diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 65b2863e37..ccdb14d0b8 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -66,6 +66,9 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se config => ConfigureCrdt(config, false)//don't add remote resources because they are added in AddCrdtRemoteResources ); services.AddCrdtRemoteResources(); + // AddCrdtRemoteResources registers its change types in a Configure callback, so finalize the + // JSON options in a PostConfigure (which runs after every Configure) — see FinalizeJsonSerializerOptions. + services.AddOptions().PostConfigure(FinalizeJsonSerializerOptions); services.AddOptions().PostConfigure((CrdtConfig crdtConfig, IOptions lcmConfig) => { crdtConfig.LocalResourceCachePath = Path.Combine(lcmConfig.Value.ProjectPath, "localResourcesCache"); @@ -395,12 +398,21 @@ public static void ConfigureCrdt(CrdtConfig config, bool addRemoteResourceEntity // you must add an instance of it to UseChangesTests.GetAllChanges() ; + if (addRemoteResourceEntity) + config.AddRemoteResourceEntity(); + } + + // Adds the legacy ExampleSentence.Translation modifier to Harmony's JSON options. Reading + // config.JsonSerializerOptions builds the lazily-created options and freezes ChangeTypeListBuilder, + // so this must run only once every change type is registered. It's therefore kept out of + // ConfigureCrdt and run once the config is complete (a PostConfigure in DI, see AddLcmCrdtClientCore; + // right before reading the options for standalone callers, see TestJsonOptions.Harmony). + // Call once per config: it wraps the existing resolver, so a second call double-adds the modifier. + internal static void FinalizeJsonSerializerOptions(CrdtConfig config) + { config.JsonSerializerOptions.TypeInfoResolver = (config.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()) .WithAddedModifier(Json.ExampleSentenceTranslationModifier); - - if (addRemoteResourceEntity) - config.AddRemoteResourceEntity(); } public static IEnumerable AllChangeTypes()