From 2ec884c1bb2ffca812bdc6279f9555db1515a452 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 09:01:51 +0700 Subject: [PATCH 1/7] Update SIL.Harmony to 0.2.1-rc.234 and adapt to new config API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new Harmony release renames CrdtConfig to SIL.Harmony.Config.HarmonyConfig and changes several APIs. This adapts LexBox to compile and run against it: - Add a `global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig;` shim to each project that references CrdtConfig, so existing references keep compiling (transitional; a full rename can follow). - ConfigureCrdt no longer reads config.JsonSerializerOptions directly — Harmony now freezes the ChangeTypeListBuilder the first time the options are built, which threw "ChangeTypeListBuilder is frozen" before AddRemoteResourceEntity. Attach the ExampleSentence translation modifier via the new deferred config.ConfigureJsonOptions hook instead. - ChangeTypes now yields RegisteredChangeType descriptors; project to .Type in AllChangeTypes and ConfigRegistrationTests. - IChange polymorphism moved out of the type-info resolver into an internal JsonConverter Harmony only adds to its own JsonSerializerOptions. Add MakeLcmCrdtExternalJsonOptions, which copies those converters onto the Web-cased sync options, and use it for the Refit client. Without it, decoding a server changes response threw "Deserialization of interface or abstract types is not supported. Type 'SIL.Harmony.Changes.IChange'". Co-Authored-By: Claude Opus 4.8 --- backend/Directory.Packages.props | 6 ++--- .../FwLite/FwLiteMaui/HarmonyConfigAlias.cs | 3 +++ .../HarmonyConfigAlias.cs | 3 +++ .../FwLiteProjectSync/HarmonyConfigAlias.cs | 3 +++ .../FwLite/FwLiteShared/HarmonyConfigAlias.cs | 3 +++ .../FwLite/FwLiteWeb/HarmonyConfigAlias.cs | 3 +++ .../LcmCrdt.Tests/ConfigRegistrationTests.cs | 3 ++- .../LcmCrdt.Tests/HarmonyConfigAlias.cs | 3 +++ backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs | 3 +++ backend/FwLite/LcmCrdt/Json.cs | 24 +++++++++++++++++ backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 27 ++++++++++++------- .../FwLite/LcmDebugger/HarmonyConfigAlias.cs | 3 +++ .../Testing/FwHeadless/HarmonyConfigAlias.cs | 3 +++ .../Services/SyncWorkerTestHarness.cs | 4 +-- 14 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs create mode 100644 backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs create mode 100644 backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs create mode 100644 backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs create mode 100644 backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs create mode 100644 backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs create mode 100644 backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs create mode 100644 backend/Testing/FwHeadless/HarmonyConfigAlias.cs diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index 10bf027b84..ecc9adbecb 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -109,9 +109,9 @@ - - - + + + diff --git a/backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs index 1529c7ed97..bb5456cdb4 100644 --- a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs @@ -57,7 +57,8 @@ public void AllChangesAreRegistered() .Append(typeof(DeleteChange<>)) .ToLookup(t => t.IsGenericType); allChangeTypes.Should().NotBeEmpty(); - var registeredChangeTypes = _config.ChangeTypes.ToHashSet(); + // Harmony's ChangeTypes now yields RegisteredChangeType descriptors; project back to the CLR types. + var registeredChangeTypes = _config.ChangeTypes.Select(t => t.Type).ToHashSet(); using var _ = new AssertionScope(); //non generics foreach (var changeType in allChangeTypes[false]) diff --git a/backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs b/backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs b/backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/LcmCrdt/Json.cs b/backend/FwLite/LcmCrdt/Json.cs index 0ba71452ee..da73b30b05 100644 --- a/backend/FwLite/LcmCrdt/Json.cs +++ b/backend/FwLite/LcmCrdt/Json.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using System.Reflection; +using System.Text.Json; using System.Text.Json.Serialization.Metadata; using LinqToDB; using LinqToDB.Internal.SqlQuery; @@ -245,6 +246,29 @@ public static IJsonTypeInfoResolver MakeLcmCrdtExternalJsonTypeResolver(this Crd return resolver; } + /// + /// Web-cased for talking to the sync server (camelCase wire format), + /// with the external MiniLcm modifiers layered on. + /// + /// + /// SIL.Harmony moved polymorphism from the type-info resolver + /// into an internal JsonConverter<IChange> that it only adds to its own + /// . A resolver alone can therefore no longer read/write + /// changes, so we copy Harmony's converters onto our Web-cased options. Without this, deserializing a + /// server response throws "Deserialization of interface or abstract types is not supported. Type + /// 'SIL.Harmony.Changes.IChange'". + /// + public static JsonSerializerOptions MakeLcmCrdtExternalJsonOptions(this CrdtConfig config) + { + var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + TypeInfoResolver = config.MakeLcmCrdtExternalJsonTypeResolver() + }; + foreach (var converter in config.JsonSerializerOptions.Converters) + options.Converters.Add(converter); + return options; + } + internal static void ExampleSentenceTranslationModifier(JsonTypeInfo typeInfo) { if (typeInfo.Type == typeof(ExampleSentence)) diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 4e5f4be1d3..06065284a7 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -86,11 +86,8 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se services.AddHttpClient(); services.AddSingleton(provider => new RefitSettings { - ContentSerializer = new SystemTextJsonContentSerializer(new(JsonSerializerDefaults.Web) - { - TypeInfoResolver = provider.GetRequiredService>().Value - .MakeLcmCrdtExternalJsonTypeResolver() - }) + ContentSerializer = new SystemTextJsonContentSerializer( + provider.GetRequiredService>().Value.MakeLcmCrdtExternalJsonOptions()) }); services.AddSingleton(); services.AddSingleton(); @@ -394,9 +391,20 @@ public static void ConfigureCrdt(CrdtConfig config, bool addRemoteResourceEntity // you must add an instance of it to UseChangesTests.GetAllChanges() ; - config.JsonSerializerOptions.TypeInfoResolver = - (config.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()) - .WithAddedModifier(Json.ExampleSentenceTranslationModifier); + // Attach the legacy ExampleSentence translation modifier via Harmony's deferred JSON hook. + // Do NOT read config.JsonSerializerOptions directly here: SIL.Harmony now freezes the + // ChangeTypeListBuilder the first time the options are built, which would happen before the + // AddRemoteResourceEntity call below (and AddCrdtRemoteResources in DI) and throw + // "ChangeTypeListBuilder is frozen". ConfigureJsonOptions defers the tweak until options are built. + config.ConfigureJsonOptions(options => + { + // Append to Harmony's existing resolver rather than replacing it (see ConfigureJsonOptions docs). + if (options.TypeInfoResolver is DefaultJsonTypeInfoResolver resolver) + resolver.Modifiers.Add(Json.ExampleSentenceTranslationModifier); + else + options.TypeInfoResolver = (options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()) + .WithAddedModifier(Json.ExampleSentenceTranslationModifier); + }); if (addRemoteResourceEntity) config.AddRemoteResourceEntity(); @@ -406,7 +414,8 @@ public static IEnumerable AllChangeTypes() { var crdtConfig = new CrdtConfig(); ConfigureCrdt(crdtConfig); - return crdtConfig.ChangeTypes; + // Harmony's ChangeTypes now yields RegisteredChangeType descriptors; project back to the CLR types. + return crdtConfig.ChangeTypes.Select(t => t.Type); } public static IEnumerable AllObjectTypes() diff --git a/backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs b/backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/Testing/FwHeadless/HarmonyConfigAlias.cs b/backend/Testing/FwHeadless/HarmonyConfigAlias.cs new file mode 100644 index 0000000000..200277715a --- /dev/null +++ b/backend/Testing/FwHeadless/HarmonyConfigAlias.cs @@ -0,0 +1,3 @@ +// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. +// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. +global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs b/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs index 2737455254..71d91f8cdc 100644 --- a/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs +++ b/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs @@ -246,7 +246,7 @@ private ServiceProvider BuildServiceProvider( // SyncWorker only passes this through to MediaFileService. services.AddSingleton(_ => - new Mock(MockBehavior.Loose, null!, null!, Options.Create(new SIL.Harmony.CrdtConfig()), null!, null!, NullLogger.Instance).Object); + new Mock(MockBehavior.Loose, null!, null!, Options.Create(new CrdtConfig()), null!, null!, NullLogger.Instance).Object); var syncService = new Mock( MockBehavior.Strict, @@ -274,7 +274,7 @@ private ServiceProvider BuildServiceProvider( services.AddSingleton(syncService.Object); - var snapshotService = new Mock(MockBehavior.Strict, Options.Create(new SIL.Harmony.CrdtConfig())); + var snapshotService = new Mock(MockBehavior.Strict, Options.Create(new CrdtConfig())); snapshotService .Setup(s => s.GetProjectSnapshot(It.IsAny())) .Callback(() => Steps.Add(GetSnapshot)) From fb0e07fa5215cb2770e04d4db0fe746f53416681 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 09:18:19 +0700 Subject: [PATCH 2/7] Add AllRegisteredChanges; use Harmony discriminators for change-type lists - Add LcmCrdtKernel.AllRegisteredChanges() returning Harmony's RegisteredChangeType descriptors directly; AllChangeTypes() now just projects .Type off it. - ChangeTypesCodeGenerator and HistoryService.ListActivityChangeTypes now read the authoritative $type discriminator from RegisteredChangeType.Discriminator instead of re-deriving it, so the generated list and activity keys can't drift from what the serializer actually writes. - Remove HistoryService.GetChangeTypeKeyFromType, which existed only to share that now-unnecessary derivation. Generated TS is unchanged (discriminators identical). Co-Authored-By: Claude Opus 4.8 --- .../TypeGen/ChangeTypesCodeGenerator.cs | 10 ++++---- backend/FwLite/LcmCrdt/HistoryService.cs | 25 ++++--------------- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 16 +++++++++--- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/backend/FwLite/FwLiteShared/TypeGen/ChangeTypesCodeGenerator.cs b/backend/FwLite/FwLiteShared/TypeGen/ChangeTypesCodeGenerator.cs index 9b3dc10380..69964afb50 100644 --- a/backend/FwLite/FwLiteShared/TypeGen/ChangeTypesCodeGenerator.cs +++ b/backend/FwLite/FwLiteShared/TypeGen/ChangeTypesCodeGenerator.cs @@ -7,16 +7,16 @@ namespace FwLiteShared.TypeGen; /// /// Emits a `ChangeType` string-literal union and a `knownChangeTypes` array built from the registered CRDT -/// change types () and each type's serialized $type -/// discriminator (). Attached to the ChangeType -/// marker; suppresses the marker's own output. +/// change types and each type's serialized $type discriminator, taken straight from Harmony's +/// registration () so the generated list can't drift from +/// what the serializer writes. Attached to the ChangeType marker; suppresses the marker's own output. /// public class ChangeTypesCodeGenerator : ClassCodeGenerator { public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver) { - var typeNames = LcmCrdtKernel.AllChangeTypes() - .Select(HistoryService.GetChangeTypeKeyFromType) + var typeNames = LcmCrdtKernel.AllRegisteredChanges() + .Select(c => c.Discriminator) .Distinct() .OrderBy(name => name, StringComparer.Ordinal) .ToArray(); diff --git a/backend/FwLite/LcmCrdt/HistoryService.cs b/backend/FwLite/LcmCrdt/HistoryService.cs index a553f40034..89914fe874 100644 --- a/backend/FwLite/LcmCrdt/HistoryService.cs +++ b/backend/FwLite/LcmCrdt/HistoryService.cs @@ -138,11 +138,11 @@ public async Task ListActivityChangeTypes() .Select(g => new KeyValuePair(g.Key.ChangeTypeKey, g.Count())) .ToDictionaryAsyncLinqToDB(p => p.Key, p => p.Value); - var registeredTypes = LcmCrdtKernel.AllChangeTypes() - .Select(t => new ActivityChangeType( - GetChangeTypeKeyFromType(t), - ChangeTypeLabel(t), - changeCounts.GetValueOrDefault(GetChangeTypeKeyFromType(t)))) + var registeredTypes = LcmCrdtKernel.AllRegisteredChanges() + .Select(c => new ActivityChangeType( + c.Discriminator, + ChangeTypeLabel(c.Type), + changeCounts.GetValueOrDefault(c.Discriminator))) .Where(t => t.CommitCount > 0) .OrderBy(t => t.Label) .ToArray(); @@ -241,21 +241,6 @@ private static IQueryable ApplyActivitySort(IQueryable commits, }; } - /// - /// The serialized $type discriminator of a change type. Mirrors Harmony's own logic: generic/shared - /// changes (jsonPatch:, delete:, SetOrderChange:) define a custom static TypeName, while a dedicated - /// change class serializes under its CLR type name. Single source of truth — the ChangeTypes TS codegen - /// uses this too, so the generated list can't drift from the runtime discriminators. - /// - public static string GetChangeTypeKeyFromType(Type changeType) - { - var typeNameProp = changeType.GetProperty(nameof(IPolyType.TypeName), - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.FlattenHierarchy); - if (typeNameProp?.GetValue(null) is string name) - return name; - return changeType.Name; - } - public static string ChangeTypeLabel(Type changeType) { if (changeType.IsGenericType && changeType.Name.Contains("JsonPatch", StringComparison.Ordinal)) diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 06065284a7..9745c28e8e 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -1,6 +1,7 @@ using System.Linq.Expressions; using System.Text.Json; using SIL.Harmony; +using SIL.Harmony.Config; using SIL.Harmony.Linq2db; using SIL.Harmony.Core; using SIL.Harmony.Changes; @@ -410,12 +411,21 @@ public static void ConfigureCrdt(CrdtConfig config, bool addRemoteResourceEntity config.AddRemoteResourceEntity(); } - public static IEnumerable AllChangeTypes() + /// + /// The registered CRDT change types together with their serialized $type discriminators, straight + /// from Harmony's registration. Prefer this over when you need the + /// discriminator, so it can't drift from what the serializer actually writes. + /// + public static IReadOnlyList AllRegisteredChanges() { var crdtConfig = new CrdtConfig(); ConfigureCrdt(crdtConfig); - // Harmony's ChangeTypes now yields RegisteredChangeType descriptors; project back to the CLR types. - return crdtConfig.ChangeTypes.Select(t => t.Type); + return crdtConfig.ChangeTypes; + } + + public static IEnumerable AllChangeTypes() + { + return AllRegisteredChanges().Select(t => t.Type); } public static IEnumerable AllObjectTypes() From 6bd8196056fe8d878538e86cca56da694041345f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 09:56:29 +0700 Subject: [PATCH 3/7] Rename CrdtConfig to HarmonyConfig and drop the alias shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the transitional `global using CrdtConfig = HarmonyConfig` shims with the real rename now that we build against the renamed SIL.Harmony type: - Rename every CrdtConfig type reference to HarmonyConfig and add the `using SIL.Harmony.Config;` import the alias was masking. - Rename the corresponding `crdtConfig` variables/parameters/fields (and their doc comments) to `harmonyConfig`. LcmCrdtConfig — an unrelated local type — is left untouched, including WebTroubleshootingService's `crdtConfig` which holds an LcmCrdtConfig. - Delete the nine HarmonyConfigAlias.cs shim files. Purely mechanical; no behavior change. Generated TypeScript is unchanged. Co-Authored-By: Claude Opus 4.8 --- backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs | 3 ++- .../FwLite/FwLiteMaui/HarmonyConfigAlias.cs | 3 --- .../HarmonyConfigAlias.cs | 3 --- .../FwLiteProjectSync/HarmonyConfigAlias.cs | 3 --- .../ProjectSnapshotService.cs | 9 +++++---- .../FwLite/FwLiteShared/FwLiteSharedKernel.cs | 5 +++-- .../FwLite/FwLiteShared/HarmonyConfigAlias.cs | 3 --- .../FwLiteShared/Services/FwLiteJson.cs | 5 +++-- .../TypeGen/ReinforcedFwLiteTypingConfig.cs | 3 ++- backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs | 5 +++-- .../FwLite/FwLiteWeb/HarmonyConfigAlias.cs | 3 --- .../LcmCrdt.Tests/ConfigRegistrationTests.cs | 5 +++-- .../LcmCrdt.Tests/Data/MigrationTests.cs | 5 +++-- .../LcmCrdt.Tests/DataModelSnapshotTests.cs | 5 +++-- .../LcmCrdt.Tests/EntityCopyMethodTests.cs | 7 ++++--- .../LcmCrdt.Tests/HarmonyConfigAlias.cs | 3 --- .../FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs | 3 ++- .../FwLite/LcmCrdt.Tests/TestJsonOptions.cs | 5 +++-- backend/FwLite/LcmCrdt/CrdtProjectsService.cs | 5 +++-- .../LcmCrdt/Data/SetupCollationInterceptor.cs | 5 +++-- backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs | 3 --- backend/FwLite/LcmCrdt/Json.cs | 7 ++++--- backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs | 3 ++- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 20 +++++++++---------- .../LcmCrdt/MediaServer/LcmMediaService.cs | 5 +++-- .../FwLite/LcmCrdt/SnapshotAtCommitService.cs | 5 +++-- backend/FwLite/LcmDebugger/FakeSyncSource.cs | 3 ++- .../FwLite/LcmDebugger/HarmonyConfigAlias.cs | 3 --- .../Testing/FwHeadless/HarmonyConfigAlias.cs | 3 --- .../Services/SyncWorkerTestHarness.cs | 5 +++-- 30 files changed, 69 insertions(+), 76 deletions(-) delete mode 100644 backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs delete mode 100644 backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs delete mode 100644 backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs delete mode 100644 backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs delete mode 100644 backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs delete mode 100644 backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs delete mode 100644 backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs delete mode 100644 backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs delete mode 100644 backend/Testing/FwHeadless/HarmonyConfigAlias.cs diff --git a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs index 4ebbf51797..45a3ec6cdb 100644 --- a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs +++ b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using FwLiteMaui.Services; using FwLiteShared; using FwLiteShared.Auth; @@ -116,7 +117,7 @@ public static void AddFwLiteMauiServices(this IServiceCollection services, config.CacheFileName = fwLiteMauiConfig.AuthCacheFilePath; config.SystemWebViewLogin = true; }); - services.Configure(config => + services.Configure(config => { config.FailedSyncOutputPath = Path.Combine(baseDataPath, "failedSyncs"); config.LocalResourceCachePath = Path.Combine(baseDataPath, "localResourcesCache"); diff --git a/backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/FwLiteMaui/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/FwLiteProjectSync.Tests/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/FwLiteProjectSync/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteProjectSync/ProjectSnapshotService.cs b/backend/FwLite/FwLiteProjectSync/ProjectSnapshotService.cs index e25e7a3cdd..15be672884 100644 --- a/backend/FwLite/FwLiteProjectSync/ProjectSnapshotService.cs +++ b/backend/FwLite/FwLiteProjectSync/ProjectSnapshotService.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Text.Json; using FwDataMiniLcmBridge; using LcmCrdt; @@ -7,17 +8,17 @@ namespace FwLiteProjectSync; -public class ProjectSnapshotService(IOptions crdtConfig) +public class ProjectSnapshotService(IOptions harmonyConfig) { public virtual async Task GetProjectSnapshot(FwDataProject project) { var snapshotPath = SnapshotPath(project); if (!File.Exists(snapshotPath)) return null; await using var file = File.OpenRead(snapshotPath); - // crdtConfig's options are fine for reading even though they "exclude" [MiniLcmInternal] members: + // harmonyConfig's options are fine for reading even though they "exclude" [MiniLcmInternal] members: // the modifier only nulls the getter (the write side), so deserialization still populates those // members (Order, entity Ids) via their setters. See SaveProjectSnapshot for why they're written. - return await JsonSerializer.DeserializeAsync(file, crdtConfig.Value.JsonSerializerOptions); + return await JsonSerializer.DeserializeAsync(file, harmonyConfig.Value.JsonSerializerOptions); } public virtual async Task RegenerateProjectSnapshot(IMiniLcmReadApi crdtApi, FwDataProject project, bool keepBackup) @@ -52,7 +53,7 @@ internal static async Task SaveProjectSnapshot(FwDataProject project, ProjectSna } await using var file = File.Create(snapshotPath); - // Serialize with default options, not crdtConfig's: the CRDT options hide [MiniLcmInternal] members + // Serialize with default options, not harmonyConfig's: the CRDT options hide [MiniLcmInternal] members // (the internal Order values and entity Ids) — that's the API's presentation view, which omits // bookkeeping callers don't need. The snapshot is a stored record, so we keep the full object graph. // The sync diff itself keys off business fields and list order, not these, so this is about a diff --git a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs index 4c37077ffd..cdbbc225d1 100644 --- a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs +++ b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Net; using FwLiteShared.AppUpdate; using FwLiteShared.Auth; @@ -56,8 +57,8 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service services.AddOptions().BindConfiguration("FwLite"); services.DecorateConstructor((provider, runtime) => { - var crdtConfig = provider.GetRequiredService>().Value; - runtime.ConfigureJsonSerializerOptions(crdtConfig); + var harmonyConfig = provider.GetRequiredService>().Value; + runtime.ConfigureJsonSerializerOptions(harmonyConfig); }); return services; } diff --git a/backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/FwLiteShared/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/FwLiteShared/Services/FwLiteJson.cs b/backend/FwLite/FwLiteShared/Services/FwLiteJson.cs index e84a843c72..be01d63974 100644 --- a/backend/FwLite/FwLiteShared/Services/FwLiteJson.cs +++ b/backend/FwLite/FwLiteShared/Services/FwLiteJson.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization.Metadata; @@ -12,7 +13,7 @@ public static class FwLiteJson private static readonly PropertyInfo? _jsRuntimeJsonOptionsProperty = typeof(JSRuntime).GetProperty("JsonSerializerOptions", BindingFlags.NonPublic | BindingFlags.Instance); - public static void ConfigureJsonSerializerOptions(this IJSRuntime jsRuntime, CrdtConfig crdtConfig) + public static void ConfigureJsonSerializerOptions(this IJSRuntime jsRuntime, HarmonyConfig harmonyConfig) { //this is not supported, see: https://github.com/dotnet/aspnetcore/issues/12685 //doing it anyway, otherwise our serialization will be broken @@ -21,7 +22,7 @@ public static void ConfigureJsonSerializerOptions(this IJSRuntime jsRuntime, Crd var options = (JsonSerializerOptions?)_jsRuntimeJsonOptionsProperty.GetValue(jsRuntime, null); if (options is null) throw new InvalidOperationException("JSRuntime.JsonSerializerOptions returned null"); options.TypeInfoResolver = (options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()) - .WithAddedModifier(crdtConfig.MakeJsonTypeModifier()) + .WithAddedModifier(harmonyConfig.MakeJsonTypeModifier()) .AddExternalMiniLcmModifiers(); } } diff --git a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs index d4d4427eba..ac12543e8d 100644 --- a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs +++ b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Drawing; using System.Reflection; using System.Text.Json.Serialization; @@ -80,7 +81,7 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder) builder.ExportAsThirdParty().WithName("IRichMultiString").Imports([ new() { From = "$lib/dotnet-types/i-multi-string", Target = "type {IRichMultiString}" } ]); - var config = new CrdtConfig(); + var config = new HarmonyConfig(); LcmCrdtKernel.ConfigureCrdt(config); builder.ExportAsInterfaces([ ..config.ObjectTypes, diff --git a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs index d02dc4bce9..a6dd537eca 100644 --- a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs +++ b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using FwDataMiniLcmBridge; using FwLiteProjectSync; using SIL.Harmony; @@ -29,9 +30,9 @@ public static IServiceCollection AddFwLiteWebServices(this IServiceCollection se services.AddSingleton(); services.AddOptions().BindConfiguration("FwLiteWeb"); - services.AddOptions().PostConfigure>((jsonOptions, crdtConfig) => + services.AddOptions().PostConfigure>((jsonOptions, harmonyConfig) => { - jsonOptions.SerializerOptions.TypeInfoResolver = crdtConfig.Value.MakeLcmCrdtExternalJsonTypeResolver(); + jsonOptions.SerializerOptions.TypeInfoResolver = harmonyConfig.Value.MakeLcmCrdtExternalJsonTypeResolver(); }); return services; } diff --git a/backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs b/backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/FwLiteWeb/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs index bb5456cdb4..44729186da 100644 --- a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using FluentAssertions.Execution; using LcmCrdt.Changes; using LcmCrdt.Changes.Entries; @@ -23,11 +24,11 @@ public class ConfigRegistrationTests typeof(DeleteChange>)//Not used, instead DeleteRemoteResourceChange is used ]; - private readonly CrdtConfig _config; + private readonly HarmonyConfig _config; public ConfigRegistrationTests() { - _config = new CrdtConfig(); + _config = new HarmonyConfig(); LcmCrdtKernel.ConfigureCrdt(_config); } diff --git a/backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs b/backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs index b8c985e6c4..6ea30015d6 100644 --- a/backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json; @@ -60,7 +61,7 @@ public async Task VerifyAfterMigrationFromScriptedDb(RegressionTestHelper.Regres { await _helper.InitializeAsync(regressionVersion); var api = _helper.Services.GetRequiredService(); - var crdtConfig = _helper.Services.GetRequiredService>().Value; + var harmonyConfig = _helper.Services.GetRequiredService>().Value; await using var dbContext = await _helper.Services.GetRequiredService().CreateDbContextAsync(); var snapshots = await dbContext.Snapshots.AsNoTracking() @@ -112,7 +113,7 @@ public async Task VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb(Regress { await _helper.InitializeAsync(regressionVersion); var api = _helper.Services.GetRequiredService(); - var crdtConfig = _helper.Services.GetRequiredService>().Value; + var harmonyConfig = _helper.Services.GetRequiredService>().Value; await using var dbContext = await _helper.Services.GetRequiredService().CreateDbContextAsync(); await using var dataModel = _helper.Services.GetRequiredService(); diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs index 57b4f9f19a..30c8765293 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using FluentAssertions.Execution; @@ -20,7 +21,7 @@ public class DataModelSnapshotTests : IAsyncLifetime protected readonly AsyncServiceScope _services; private readonly LcmCrdtDbContext _crdtDbContext; - private CrdtConfig _crdtConfig; + private HarmonyConfig _crdtConfig; private CrdtProject _crdtProject; private readonly JsonSerializerOptions _jsonSerializerOptions = TestJsonOptions.Harmony(); @@ -33,7 +34,7 @@ public DataModelSnapshotTests() .BuildServiceProvider(); _services = services.CreateAsyncScope(); _crdtDbContext = _services.ServiceProvider.GetRequiredService>().CreateDbContext(); - _crdtConfig = _services.ServiceProvider.GetRequiredService>().Value; + _crdtConfig = _services.ServiceProvider.GetRequiredService>().Value; } public async Task InitializeAsync() diff --git a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs index ec34d2245c..ce1145d018 100644 --- a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using FluentAssertions.Equivalency; using FluentAssertions.Execution; using MiniLcm.Media; @@ -14,9 +15,9 @@ public class EntityCopyMethodTests public static IEnumerable GetEntityTypes() { - var crdtConfig = new CrdtConfig(); - LcmCrdtKernel.ConfigureCrdt(crdtConfig); - return crdtConfig.ObjectTypes + var harmonyConfig = new HarmonyConfig(); + LcmCrdtKernel.ConfigureCrdt(harmonyConfig); + return harmonyConfig.ObjectTypes .Except([typeof(RemoteResource)])//exclude remote resource as it's a harmony defined type, not miniLcm .Select(t => new object[] { t }); } diff --git a/backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs b/backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/LcmCrdt.Tests/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs index 0926263cd0..e30f61cb67 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Diagnostics; using LcmCrdt.MediaServer; using LcmCrdt.Objects; @@ -20,7 +21,7 @@ public class MiniLcmApiFixture : IAsyncLifetime, IAsyncDisposable public CrdtMiniLcmApi Api => (CrdtMiniLcmApi)_services.ServiceProvider.GetRequiredService(); public DataModel DataModel => _services.ServiceProvider.GetRequiredService(); public LcmCrdtDbContext DbContext => _crdtDbContext ?? throw new InvalidOperationException("MiniLcmApiFixture not initialized"); - public CrdtConfig CrdtConfig => _services.ServiceProvider.GetRequiredService>().Value; + public HarmonyConfig HarmonyConfig => _services.ServiceProvider.GetRequiredService>().Value; public T GetService() where T : notnull { diff --git a/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs b/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs index 76bc8ad5a3..55c9b95795 100644 --- a/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs +++ b/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Text.Json; namespace LcmCrdt.Tests; @@ -6,7 +7,7 @@ public static class TestJsonOptions { public static JsonSerializerOptions External() { - var config = new CrdtConfig(); + var config = new HarmonyConfig(); LcmCrdtKernel.ConfigureCrdt(config); return new JsonSerializerOptions(JsonSerializerDefaults.General) { @@ -16,7 +17,7 @@ public static JsonSerializerOptions External() public static JsonSerializerOptions Harmony() { - var config = new CrdtConfig(); + var config = new HarmonyConfig(); LcmCrdtKernel.ConfigureCrdt(config); return config.JsonSerializerOptions; } diff --git a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs index a7aa058dc7..42506f26da 100644 --- a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs +++ b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; @@ -209,7 +210,7 @@ public virtual async Task CreateProjectFromTemplate( AfterCreate = async (provider, project) => { var api = provider.GetRequiredService(); - var jsonOptions = provider.GetRequiredService>().Value.JsonSerializerOptions; + var jsonOptions = provider.GetRequiredService>().Value.JsonSerializerOptions; var snapshot = ProjectTemplate.CreateNewSnapshot(jsonOptions, vernacularWs, analysisWs); var commitMetadataInterceptor = provider.GetRequiredService(); using (commitMetadataInterceptor.Intercept(CommitHelpers.StampAsTemplate)) @@ -344,7 +345,7 @@ private Task EnsureDeleteProject(string sqliteFile, bool suppressException = fal public async Task DeleteProject(string code) { var project = GetProject(code) ?? throw new InvalidOperationException($"Project {code} not found"); - var projectResourceCachePath = LcmMediaService.ProjectCachePath(project, provider.GetRequiredService>().Value); + var projectResourceCachePath = LcmMediaService.ProjectCachePath(project, provider.GetRequiredService>().Value); if (Directory.Exists(projectResourceCachePath)) Directory.Delete(projectResourceCachePath, true); await EnsureDeleteProject(project.DbPath); } diff --git a/backend/FwLite/LcmCrdt/Data/SetupCollationInterceptor.cs b/backend/FwLite/LcmCrdt/Data/SetupCollationInterceptor.cs index de1a1c856e..1f9fbd6042 100644 --- a/backend/FwLite/LcmCrdt/Data/SetupCollationInterceptor.cs +++ b/backend/FwLite/LcmCrdt/Data/SetupCollationInterceptor.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Data; using System.Data.Common; using System.Globalization; @@ -13,7 +14,7 @@ namespace LcmCrdt.Data; -public class SetupCollationInterceptor(IMemoryCache cache, IMiniLcmCultureProvider cultureProvider, IOptions crdtConfig) : IDbConnectionInterceptor, ISaveChangesInterceptor, IConnectionInterceptor +public class SetupCollationInterceptor(IMemoryCache cache, IMiniLcmCultureProvider cultureProvider, IOptions harmonyConfig) : IDbConnectionInterceptor, ISaveChangesInterceptor, IConnectionInterceptor { private static string? WsTableName = null; private WritingSystem[] GetWritingSystems(DbConnection connection, LcmCrdtDbContext? dbContext = null) @@ -29,7 +30,7 @@ private WritingSystem[] GetWritingSystems(DbConnection connection, LcmCrdtDbCont { var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite(connection); - localContext = new LcmCrdtDbContext(optionsBuilder.Options, crdtConfig); + localContext = new LcmCrdtDbContext(optionsBuilder.Options, harmonyConfig); } try diff --git a/backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs b/backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/LcmCrdt/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/FwLite/LcmCrdt/Json.cs b/backend/FwLite/LcmCrdt/Json.cs index da73b30b05..763b413958 100644 --- a/backend/FwLite/LcmCrdt/Json.cs +++ b/backend/FwLite/LcmCrdt/Json.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Linq.Expressions; using System.Reflection; using System.Text.Json; @@ -239,7 +240,7 @@ internal record JsonEach( [property: Column("fullkey")] string FullKey, [property: Column("path")] string Path); - public static IJsonTypeInfoResolver MakeLcmCrdtExternalJsonTypeResolver(this CrdtConfig config) + public static IJsonTypeInfoResolver MakeLcmCrdtExternalJsonTypeResolver(this HarmonyConfig config) { var resolver = config.MakeJsonTypeResolver(); resolver = resolver.AddExternalMiniLcmModifiers(); @@ -253,12 +254,12 @@ public static IJsonTypeInfoResolver MakeLcmCrdtExternalJsonTypeResolver(this Crd /// /// SIL.Harmony moved polymorphism from the type-info resolver /// into an internal JsonConverter<IChange> that it only adds to its own - /// . A resolver alone can therefore no longer read/write + /// . A resolver alone can therefore no longer read/write /// changes, so we copy Harmony's converters onto our Web-cased options. Without this, deserializing a /// server response throws "Deserialization of interface or abstract types is not supported. Type /// 'SIL.Harmony.Changes.IChange'". /// - public static JsonSerializerOptions MakeLcmCrdtExternalJsonOptions(this CrdtConfig config) + public static JsonSerializerOptions MakeLcmCrdtExternalJsonOptions(this HarmonyConfig config) { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { diff --git a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs index e1fcf6eac3..350edc3cf7 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.ComponentModel; using System.Text.Json; using LcmCrdt.Data; @@ -12,7 +13,7 @@ namespace LcmCrdt; public class LcmCrdtDbContext( DbContextOptions dbContextOptions, - IOptions options + IOptions options ) : DbContext(dbContextOptions), ICrdtDbContext { diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 9745c28e8e..5922d26bb3 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -67,9 +67,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(); - services.AddOptions().PostConfigure((CrdtConfig crdtConfig, IOptions lcmConfig) => + services.AddOptions().PostConfigure((HarmonyConfig harmonyConfig, IOptions lcmConfig) => { - crdtConfig.LocalResourceCachePath = Path.Combine(lcmConfig.Value.ProjectPath, "localResourcesCache"); + harmonyConfig.LocalResourceCachePath = Path.Combine(lcmConfig.Value.ProjectPath, "localResourcesCache"); }); services.AddScoped(); services.AddScoped(); @@ -88,7 +88,7 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se services.AddSingleton(provider => new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer( - provider.GetRequiredService>().Value.MakeLcmCrdtExternalJsonOptions()) + provider.GetRequiredService>().Value.MakeLcmCrdtExternalJsonOptions()) }); services.AddSingleton(); services.AddSingleton(); @@ -185,7 +185,7 @@ private static Expression>> EntryPublishInRo return e => Json.Query(e.PublishIn); } - public static void ConfigureCrdt(CrdtConfig config, bool addRemoteResourceEntity = true) + public static void ConfigureCrdt(HarmonyConfig config, bool addRemoteResourceEntity = true) { config.EnableProjectedTables = true; config.ObjectTypeListBuilder @@ -418,9 +418,9 @@ public static void ConfigureCrdt(CrdtConfig config, bool addRemoteResourceEntity /// public static IReadOnlyList AllRegisteredChanges() { - var crdtConfig = new CrdtConfig(); - ConfigureCrdt(crdtConfig); - return crdtConfig.ChangeTypes; + var harmonyConfig = new HarmonyConfig(); + ConfigureCrdt(harmonyConfig); + return harmonyConfig.ChangeTypes; } public static IEnumerable AllChangeTypes() @@ -430,9 +430,9 @@ public static IEnumerable AllChangeTypes() public static IEnumerable AllObjectTypes() { - var crdtConfig = new CrdtConfig(); - ConfigureCrdt(crdtConfig); - return crdtConfig.ObjectTypes; + var harmonyConfig = new HarmonyConfig(); + ConfigureCrdt(harmonyConfig); + return harmonyConfig.ObjectTypes; } private static IList DeserializeTranslations(string json) diff --git a/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs b/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs index 926ee24564..62060ca70e 100644 --- a/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs +++ b/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Collections.Concurrent; using System.Net; using System.Net.Mime; @@ -16,7 +17,7 @@ namespace LcmCrdt.MediaServer; public class LcmMediaService( ResourceService resourceService, CurrentProjectService currentProjectService, - IOptions options, + IOptions options, IRefitHttpServiceFactory refitFactory, IServerHttpClientProvider httpClientProvider, ILogger logger @@ -189,7 +190,7 @@ async Task IRemoteResourceService.DownloadResou public string ProjectResourceCachePath => ProjectCachePath(currentProjectService.Project, options.Value); - public static string ProjectCachePath(CrdtProject project, CrdtConfig options) + public static string ProjectCachePath(CrdtProject project, HarmonyConfig options) { return Path.Combine(options.LocalResourceCachePath, project.Name); } diff --git a/backend/FwLite/LcmCrdt/SnapshotAtCommitService.cs b/backend/FwLite/LcmCrdt/SnapshotAtCommitService.cs index ddf2ec2f47..f86fe701b3 100644 --- a/backend/FwLite/LcmCrdt/SnapshotAtCommitService.cs +++ b/backend/FwLite/LcmCrdt/SnapshotAtCommitService.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Diagnostics; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -14,7 +15,7 @@ namespace LcmCrdt; public class SnapshotAtCommitService( IServiceProvider serviceProvider, - IOptions crdtConfig, + IOptions harmonyConfig, CurrentProjectService currentProjectService, ILogger logger, ICrdtDbContextFactory crdtDbContextFactory) @@ -51,7 +52,7 @@ public class SnapshotAtCommitService( var optionsBuilder = new DbContextOptionsBuilder() .UseSqlite($"Data Source={forkDbPath}"); LcmCrdtKernel.ConfigureDbOptions(serviceScope.ServiceProvider, optionsBuilder); - await using var forkDbContext = new LcmCrdtDbContext(optionsBuilder.Options, crdtConfig); + await using var forkDbContext = new LcmCrdtDbContext(optionsBuilder.Options, harmonyConfig); var deleted = await DeleteCommitsAfter(forkDbContext, commit, preserveAllFieldWorksCommits); logger.LogInformation("Deleted {Deleted} commits after {CommitId}", deleted, commitId); diff --git a/backend/FwLite/LcmDebugger/FakeSyncSource.cs b/backend/FwLite/LcmDebugger/FakeSyncSource.cs index f488e2a263..b26a5d1310 100644 --- a/backend/FwLite/LcmDebugger/FakeSyncSource.cs +++ b/backend/FwLite/LcmDebugger/FakeSyncSource.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using LcmCrdt; @@ -42,7 +43,7 @@ public static FakeSyncSource FromJsonFile(string path, JsonSerializerOptions? op { if (options is null) { - var config = new CrdtConfig(); + var config = new HarmonyConfig(); LcmCrdtKernel.ConfigureCrdt(config); options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { diff --git a/backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs b/backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/FwLite/LcmDebugger/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/Testing/FwHeadless/HarmonyConfigAlias.cs b/backend/Testing/FwHeadless/HarmonyConfigAlias.cs deleted file mode 100644 index 200277715a..0000000000 --- a/backend/Testing/FwHeadless/HarmonyConfigAlias.cs +++ /dev/null @@ -1,3 +0,0 @@ -// TEMPORARY compat shim: SIL.Harmony renamed CrdtConfig -> SIL.Harmony.Config.HarmonyConfig. -// This global alias lets existing `CrdtConfig` references keep compiling until they're renamed. -global using CrdtConfig = SIL.Harmony.Config.HarmonyConfig; diff --git a/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs b/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs index 71d91f8cdc..74814ff503 100644 --- a/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs +++ b/backend/Testing/FwHeadless/Services/SyncWorkerTestHarness.cs @@ -1,3 +1,4 @@ +using SIL.Harmony.Config; using FwDataMiniLcmBridge; using FwDataMiniLcmBridge.Api; using FwDataMiniLcmBridge.Tests.Fixtures; @@ -246,7 +247,7 @@ private ServiceProvider BuildServiceProvider( // SyncWorker only passes this through to MediaFileService. services.AddSingleton(_ => - new Mock(MockBehavior.Loose, null!, null!, Options.Create(new CrdtConfig()), null!, null!, NullLogger.Instance).Object); + new Mock(MockBehavior.Loose, null!, null!, Options.Create(new HarmonyConfig()), null!, null!, NullLogger.Instance).Object); var syncService = new Mock( MockBehavior.Strict, @@ -274,7 +275,7 @@ private ServiceProvider BuildServiceProvider( services.AddSingleton(syncService.Object); - var snapshotService = new Mock(MockBehavior.Strict, Options.Create(new CrdtConfig())); + var snapshotService = new Mock(MockBehavior.Strict, Options.Create(new HarmonyConfig())); snapshotService .Setup(s => s.GetProjectSnapshot(It.IsAny())) .Callback(() => Steps.Add(GetSnapshot)) From 13331131464e2b40d0de19094950ccfecce77295 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 12:00:20 +0700 Subject: [PATCH 4/7] Drop stale comment in ConfigRegistrationTests Co-Authored-By: Claude Opus 4.8 --- backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs index 44729186da..5938943f3f 100644 --- a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs @@ -58,7 +58,6 @@ public void AllChangesAreRegistered() .Append(typeof(DeleteChange<>)) .ToLookup(t => t.IsGenericType); allChangeTypes.Should().NotBeEmpty(); - // Harmony's ChangeTypes now yields RegisteredChangeType descriptors; project back to the CLR types. var registeredChangeTypes = _config.ChangeTypes.Select(t => t.Type).ToHashSet(); using var _ = new AssertionScope(); //non generics From 1f7a291ea55ce0fff13a4c2261473cf22a6635fa Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 12:13:17 +0700 Subject: [PATCH 5/7] Add IChange converter to web and debugger sync JSON options CodeRabbit flagged that FwLiteWebKernel's JsonOptions and FakeSyncSource built Web options with only Harmony's type resolver, so ChangeEntity fields in sync payloads deserialized without Harmony's IChange polymorphism converter (the same break the Refit client hit). - FwLiteWebKernel: copy both the resolver and Harmony's converters from MakeLcmCrdtExternalJsonOptions onto the ASP.NET SerializerOptions. - FakeSyncSource.FromJsonFile and TestJsonOptions.External: use MakeLcmCrdtExternalJsonOptions instead of a resolver-only options object. - Add a regression test round-tripping every change type as IChange through the external options. Co-Authored-By: Claude Opus 4.8 --- backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs | 8 +++++++- .../Changes/ChangeSerializationTests.cs | 14 ++++++++++++++ backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs | 7 +++---- backend/FwLite/LcmDebugger/FakeSyncSource.cs | 7 +++---- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs index a6dd537eca..ce39fbdcfd 100644 --- a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs +++ b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs @@ -32,7 +32,13 @@ public static IServiceCollection AddFwLiteWebServices(this IServiceCollection se services.AddOptions().PostConfigure>((jsonOptions, harmonyConfig) => { - jsonOptions.SerializerOptions.TypeInfoResolver = harmonyConfig.Value.MakeLcmCrdtExternalJsonTypeResolver(); + // Copy both the type resolver and Harmony's converters: IChange polymorphism now lives in an + // internal converter (see MakeLcmCrdtExternalJsonOptions), so the resolver alone can't decode + // ChangeEntity in sync payloads. + var externalOptions = harmonyConfig.Value.MakeLcmCrdtExternalJsonOptions(); + jsonOptions.SerializerOptions.TypeInfoResolver = externalOptions.TypeInfoResolver; + foreach (var converter in externalOptions.Converters) + jsonOptions.SerializerOptions.Converters.Add(converter); }); return services; } diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs index 524c9a8bf7..b309b3cce5 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs @@ -54,6 +54,20 @@ public void CanRoundTripChanges(IChange change) newChange.Should().BeEquivalentTo(change); } + [Theory] + [MemberData(nameof(Changes))] + public void CanRoundTripChangesAsIChangeWithExternalOptions(IChange change) + { + // The sync client, web host, and debugger all (de)serialize change fields as the abstract IChange + // using the external (Web) options. That relies on Harmony's polymorphic converter, not just the + // type resolver — regression guard against the "Deserialization of interface or abstract types is + // not supported. Type 'IChange'" break when only the resolver was wired onto the external options. + var options = TestJsonOptions.External(); + var json = JsonSerializer.Serialize(change, options); + var newChange = JsonSerializer.Deserialize(json, options); + newChange.Should().BeEquivalentTo(change); + } + [Fact] public void ChangesIncludesAllValidChangeTypes() { diff --git a/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs b/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs index 55c9b95795..d453659b59 100644 --- a/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs +++ b/backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs @@ -9,10 +9,9 @@ public static JsonSerializerOptions External() { var config = new HarmonyConfig(); LcmCrdtKernel.ConfigureCrdt(config); - return new JsonSerializerOptions(JsonSerializerDefaults.General) - { - TypeInfoResolver = config.MakeLcmCrdtExternalJsonTypeResolver(), - }; + // Full external options (resolver + Harmony's IChange converter), matching what the sync client, + // web host, and debugger use — a resolver alone can't deserialize IChange. + return config.MakeLcmCrdtExternalJsonOptions(); } public static JsonSerializerOptions Harmony() diff --git a/backend/FwLite/LcmDebugger/FakeSyncSource.cs b/backend/FwLite/LcmDebugger/FakeSyncSource.cs index b26a5d1310..b0ab3fb4d0 100644 --- a/backend/FwLite/LcmDebugger/FakeSyncSource.cs +++ b/backend/FwLite/LcmDebugger/FakeSyncSource.cs @@ -45,10 +45,9 @@ public static FakeSyncSource FromJsonFile(string path, JsonSerializerOptions? op { var config = new HarmonyConfig(); LcmCrdtKernel.ConfigureCrdt(config); - options = new JsonSerializerOptions(JsonSerializerDefaults.Web) - { - TypeInfoResolver = config.MakeLcmCrdtExternalJsonTypeResolver(), - }; + // Full external options (resolver + Harmony's IChange converter); the resolver alone can't + // deserialize the ChangeEntity fields in the captured sync payload. + options = config.MakeLcmCrdtExternalJsonOptions(); } using var file = File.OpenRead(path); From 5efd37227d388d70d75c514d4336f890e671cfe4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 13:11:20 +0700 Subject: [PATCH 6/7] fix failure message --- backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs index 30c8765293..4b4bd3d096 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs @@ -66,7 +66,7 @@ private IEnumerable GetPolymorphicTypesFor(Type type) { var polymorphismOptions = _jsonSerializerOptions.GetTypeInfo(type) .PolymorphismOptions; - polymorphismOptions.Should().NotBeNull("type {type} should only be called if it's configured properly", type); + polymorphismOptions.Should().NotBeNull("type {0} should only be called if it's configured properly", type); polymorphismOptions.TypeDiscriminatorPropertyName.Should().Be("$type"); return polymorphismOptions.DerivedTypes.OrderBy(t => t.DerivedType.FullName); } From 9565eeb5680cd80a905341ef3c34a380cd86162e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 13:14:48 +0700 Subject: [PATCH 7/7] update verify test to follow the old format --- backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs index 4b4bd3d096..a63e639545 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs @@ -75,7 +75,10 @@ private IEnumerable GetPolymorphicTypesFor(Type type) [Trait("Category", "Verified")] public async Task VerifyChangeModels() { - await Verify(GetPolymorphicTypesFor(typeof(IChange))); + await Verify(LcmCrdtKernel.AllRegisteredChanges() + //map to JsonDerivedType to match the verification format previously used + .Select(c => new JsonDerivedType(c.Type, c.Discriminator)) + .OrderBy(j => j.DerivedType.FullName)); } [Fact]