From 390e413225dbec87840bedad22d2cf87881f43a2 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 22 Oct 2024 14:21:00 +0700 Subject: [PATCH 01/15] begin introducing `BeforePersistObjectInProjectedTable` --- src/SIL.Harmony.Tests/DataModelTestBase.cs | 14 ++-- .../PersistExtraDataTests.cs | 69 +++++++++++++++++++ src/SIL.Harmony/CrdtConfig.cs | 3 +- src/SIL.Harmony/Db/CrdtRepository.cs | 9 ++- 4 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 src/SIL.Harmony.Tests/PersistExtraDataTests.cs diff --git a/src/SIL.Harmony.Tests/DataModelTestBase.cs b/src/SIL.Harmony.Tests/DataModelTestBase.cs index de2329b..23d7805 100644 --- a/src/SIL.Harmony.Tests/DataModelTestBase.cs +++ b/src/SIL.Harmony.Tests/DataModelTestBase.cs @@ -22,9 +22,10 @@ public class DataModelTestBase : IAsyncLifetime internal readonly CrdtRepository CrdtRepository; protected readonly MockTimeProvider MockTimeProvider = new(); - public DataModelTestBase(bool saveToDisk = false, bool alwaysValidate = true) : this(saveToDisk + public DataModelTestBase(bool saveToDisk = false, bool alwaysValidate = true, + Action? configure = null) : this(saveToDisk ? new SqliteConnection("Data Source=test.db") - : new SqliteConnection("Data Source=:memory:"), alwaysValidate) + : new SqliteConnection("Data Source=:memory:"), alwaysValidate, configure) { } @@ -32,14 +33,15 @@ public DataModelTestBase() : this(new SqliteConnection("Data Source=:memory:")) { } - public DataModelTestBase(SqliteConnection connection, bool alwaysValidate = true) + public DataModelTestBase(SqliteConnection connection, bool alwaysValidate = true, Action? configure = null) { - _services = new ServiceCollection() + var serviceCollection = new ServiceCollection() .AddCrdtDataSample(connection) .AddOptions().Configure(config => config.AlwaysValidateCommits = alwaysValidate) .Services - .Replace(ServiceDescriptor.Singleton(MockTimeProvider)) - .BuildServiceProvider(); + .Replace(ServiceDescriptor.Singleton(MockTimeProvider)); + configure?.Invoke(serviceCollection); + _services = serviceCollection.BuildServiceProvider(); DbContext = _services.GetRequiredService(); DbContext.Database.OpenConnection(); DbContext.Database.EnsureCreated(); diff --git a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs new file mode 100644 index 0000000..0716352 --- /dev/null +++ b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SIL.Harmony.Changes; +using SIL.Harmony.Core; +using SIL.Harmony.Entities; +using SIL.Harmony.Sample; + +namespace SIL.Harmony.Tests; + +public class PersistExtraDataTests +{ + private DataModelTestBase _dataModelTestBase; + + public class CreateExtraDataModelChange(Guid entityId) : CreateChange(entityId), ISelfNamedType + { + public override ValueTask NewEntity(Commit commit, ChangeContext context) + { + return ValueTask.FromResult(new ExtraDataModel() + { + Id = EntityId, + }); + } + } + + public class ExtraDataModel : IObjectBase + { + public Guid Id { get; set; } + public DateTimeOffset? DeletedAt { get; set; } + public Guid CommitId { get; set; } + public HybridDateTime? HybridDateTime { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, Commit commit) + { + } + + public IObjectBase Copy() + { + return new ExtraDataModel(); + } + } + + public PersistExtraDataTests() + { + _dataModelTestBase = new DataModelTestBase(configure: services => + { + services.AddOptions().Configure(config => + { + config.ObjectTypeListBuilder.DefaultAdapter().Add(); + config.ChangeTypeListBuilder.Add(); + }); + }); + } + + [Fact] + public async Task CanPersistExtraData() + { + var entityId = Guid.NewGuid(); + var commit = await _dataModelTestBase.WriteNextChange(new CreateExtraDataModelChange(entityId)); + var extraDataModel = _dataModelTestBase.DataModel.QueryLatest().Should().ContainSingle().Subject; + extraDataModel.Id.Should().Be(entityId); + extraDataModel.CommitId.Should().Be(commit.Id); + extraDataModel.HybridDateTime.Should().Be(commit.HybridDateTime); + } +} \ No newline at end of file diff --git a/src/SIL.Harmony/CrdtConfig.cs b/src/SIL.Harmony/CrdtConfig.cs index 1b9837a..bcfb6d5 100644 --- a/src/SIL.Harmony/CrdtConfig.cs +++ b/src/SIL.Harmony/CrdtConfig.cs @@ -8,7 +8,7 @@ using SIL.Harmony.Entities; namespace SIL.Harmony; - +public delegate ValueTask BeforeSaveObject(object obj, ObjectSnapshot snapshot); public class CrdtConfig { /// @@ -16,6 +16,7 @@ public class CrdtConfig /// it does however increase database size as now objects are stored both in snapshots and in their projected tables /// public bool EnableProjectedTables { get; set; } = true; + public BeforeSaveObject? BeforePersistObjectInProjectedTable { get; set; } /// /// after adding any commit validate the commit history, not great for performance but good for testing. /// diff --git a/src/SIL.Harmony/Db/CrdtRepository.cs b/src/SIL.Harmony/Db/CrdtRepository.cs index bb94e70..383e426 100644 --- a/src/SIL.Harmony/Db/CrdtRepository.cs +++ b/src/SIL.Harmony/Db/CrdtRepository.cs @@ -229,11 +229,14 @@ private async ValueTask SnapshotAdded(ObjectSnapshot objectSnapshot) if (objectSnapshot.IsRoot && objectSnapshot.EntityIsDeleted) return; //need to check if an entry exists already, even if this is the root commit it may have already been added to the db var existingEntry = await GetEntityEntry(objectSnapshot.Entity.DbObject.GetType(), objectSnapshot.EntityId); + object? entity; if (existingEntry is null && objectSnapshot.IsRoot) { //if we don't make a copy first then the entity will be tracked by the context and be modified //by future changes in the same session - _dbContext.Add((object)objectSnapshot.Entity.Copy().DbObject) + entity = objectSnapshot.Entity.Copy().DbObject; + crdtConfig.Value.BeforePersistObjectInProjectedTable?.Invoke(entity, objectSnapshot); + _dbContext.Add(entity) .Property(ObjectSnapshot.ShadowRefName).CurrentValue = objectSnapshot.Id; return; } @@ -245,7 +248,9 @@ private async ValueTask SnapshotAdded(ObjectSnapshot objectSnapshot) return; } - existingEntry.CurrentValues.SetValues(objectSnapshot.Entity.DbObject); + entity = objectSnapshot.Entity.DbObject; + crdtConfig.Value.BeforePersistObjectInProjectedTable?.Invoke(entity, objectSnapshot); + existingEntry.CurrentValues.SetValues(entity); existingEntry.Property(ObjectSnapshot.ShadowRefName).CurrentValue = objectSnapshot.Id; } From 026f483d8be2e731cd27d7c552a7bda6d7d7eca8 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 22 Oct 2024 14:21:14 +0700 Subject: [PATCH 02/15] rename GetLatestObjects to QueryLatest --- src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs | 2 +- src/SIL.Harmony.Tests/DataModelSimpleChanges.cs | 2 +- src/SIL.Harmony.Tests/DataQueryTests.cs | 2 +- src/SIL.Harmony.Tests/DefinitionTests.cs | 8 ++++---- src/SIL.Harmony.Tests/SyncTests.cs | 4 ++-- src/SIL.Harmony/DataModel.cs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs b/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs index c34d994..6af13d4 100644 --- a/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs +++ b/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs @@ -195,6 +195,6 @@ await dataModel.AddChange(Guid.NewGuid(), myClass2.MyNumber.Should().Be(123.45m); myClass2.DeletedTime.Should().BeNull(); - dataModel.GetLatestObjects().Should().NotBeEmpty(); + dataModel.QueryLatest().Should().NotBeEmpty(); } } \ No newline at end of file diff --git a/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs b/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs index 661b8fd..bf57480 100644 --- a/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs +++ b/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs @@ -79,7 +79,7 @@ public async Task WriteMultipleCommits() await WriteNextChange(SetWord(Guid.NewGuid(), "change 3")); DbContext.Snapshots.Should().HaveCount(3); - DataModel.GetLatestObjects().Should().HaveCount(3); + DataModel.QueryLatest().Should().HaveCount(3); } [Fact] diff --git a/src/SIL.Harmony.Tests/DataQueryTests.cs b/src/SIL.Harmony.Tests/DataQueryTests.cs index a2ac334..1144e4b 100644 --- a/src/SIL.Harmony.Tests/DataQueryTests.cs +++ b/src/SIL.Harmony.Tests/DataQueryTests.cs @@ -15,7 +15,7 @@ public override async Task InitializeAsync() [Fact] public async Task CanQueryLatestData() { - var entries = await DataModel.GetLatestObjects().ToArrayAsync(); + var entries = await DataModel.QueryLatest().ToArrayAsync(); var entry = entries.Should().ContainSingle().Subject; entry.Text.Should().Be("entity1"); } diff --git a/src/SIL.Harmony.Tests/DefinitionTests.cs b/src/SIL.Harmony.Tests/DefinitionTests.cs index e21e9c9..49e664c 100644 --- a/src/SIL.Harmony.Tests/DefinitionTests.cs +++ b/src/SIL.Harmony.Tests/DefinitionTests.cs @@ -50,7 +50,7 @@ public async Task CanGetInOrder() await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2)); await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1)); - var definitions = await DataModel.GetLatestObjects().ToArrayAsync(); + var definitions = await DataModel.QueryLatest().ToArrayAsync(); definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( "noun", "verb" @@ -69,7 +69,7 @@ public async Task CanChangeOrderBetweenExistingDefinitions() await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2, definitionBId)); await WriteNextChange(NewDefinition(wordId, "used as a greeting", "exclamation", 3, definitionCId)); - var definitions = await DataModel.GetLatestObjects().ToArrayAsync(); + var definitions = await DataModel.QueryLatest().ToArrayAsync(); definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( "noun", "verb", @@ -79,7 +79,7 @@ public async Task CanChangeOrderBetweenExistingDefinitions() //change the order of the exclamation to be between the noun and verb await WriteNextChange(SetOrderChange.Between(definitionCId, definitions[0], definitions[1])); - definitions = await DataModel.GetLatestObjects().ToArrayAsync(); + definitions = await DataModel.QueryLatest().ToArrayAsync(); definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( "noun", "exclamation", @@ -98,7 +98,7 @@ public async Task ConsistentlySortsItems() await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 1, definitionAId)); await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionBId)); - var definitions = await DataModel.GetLatestObjects().ToArrayAsync(); + var definitions = await DataModel.QueryLatest().ToArrayAsync(); definitions.Select(d => d.Id).Should().ContainInConsecutiveOrder( definitionBId, definitionAId diff --git a/src/SIL.Harmony.Tests/SyncTests.cs b/src/SIL.Harmony.Tests/SyncTests.cs index 214781d..385cba1 100644 --- a/src/SIL.Harmony.Tests/SyncTests.cs +++ b/src/SIL.Harmony.Tests/SyncTests.cs @@ -116,7 +116,7 @@ public async Task CanSync_AddDependentWithMultipleChanges() await _client2.DataModel.SyncWith(_client1.DataModel); - _client2.DataModel.GetLatestObjects().Should() - .BeEquivalentTo(_client1.DataModel.GetLatestObjects()); + _client2.DataModel.QueryLatest().Should() + .BeEquivalentTo(_client1.DataModel.QueryLatest()); } } \ No newline at end of file diff --git a/src/SIL.Harmony/DataModel.cs b/src/SIL.Harmony/DataModel.cs index a0e0292..4853a2d 100644 --- a/src/SIL.Harmony/DataModel.cs +++ b/src/SIL.Harmony/DataModel.cs @@ -209,7 +209,7 @@ public async Task GetProjectSnapshot(bool includeDeleted = false) return new ModelSnapshot(await _crdtRepository.CurrenSimpleSnapshots(includeDeleted).ToArrayAsync()); } - public IQueryable GetLatestObjects() where T : class + public IQueryable QueryLatest() where T : class { var q = _crdtRepository.GetCurrentObjects(); if (q is IQueryable) From 22be45ffb430a0ea54798ef77399cb8966eb4200 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 15 Oct 2024 10:05:33 +0700 Subject: [PATCH 03/15] allow multiple object adapters --- .../Adapters/CustomAdapterProvider.cs | 8 +++- .../Adapters/DefaultAdapterProvider.cs | 8 +++- .../Adapters/IObjectAdapterProvider.cs | 3 +- src/SIL.Harmony/Changes/ChangeContext.cs | 2 +- src/SIL.Harmony/CrdtConfig.cs | 37 +++++++++++++------ 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/SIL.Harmony/Adapters/CustomAdapterProvider.cs b/src/SIL.Harmony/Adapters/CustomAdapterProvider.cs index 53231cc..81a2252 100644 --- a/src/SIL.Harmony/Adapters/CustomAdapterProvider.cs +++ b/src/SIL.Harmony/Adapters/CustomAdapterProvider.cs @@ -12,8 +12,7 @@ public class CustomAdapterProvider : IObjectAd { private readonly ObjectTypeListBuilder _objectTypeListBuilder; private readonly List _objectTypes = new(); - private Dictionary> JsonTypes { get; } = []; - Dictionary> IObjectAdapterProvider.JsonTypes => JsonTypes; + private Dictionary> JsonTypes => _objectTypeListBuilder.JsonTypes; public CustomAdapterProvider(ObjectTypeListBuilder objectTypeListBuilder) { @@ -55,6 +54,11 @@ IObjectBase IObjectAdapterProvider.Adapt(object obj) { return TCustomAdapter.Create((TCommonInterface)obj); } + + public bool CanAdapt(object obj) + { + return obj is TCommonInterface; + } } // it's possible to implement this without a Common interface diff --git a/src/SIL.Harmony/Adapters/DefaultAdapterProvider.cs b/src/SIL.Harmony/Adapters/DefaultAdapterProvider.cs index 53bea0f..b764ba9 100644 --- a/src/SIL.Harmony/Adapters/DefaultAdapterProvider.cs +++ b/src/SIL.Harmony/Adapters/DefaultAdapterProvider.cs @@ -38,6 +38,10 @@ IObjectBase IObjectAdapterProvider.Adapt(object obj) $"Object is of type {obj.GetType().Name} which does not implement {nameof(IObjectBase)}"); } - private Dictionary> JsonTypes { get; } = []; - Dictionary> IObjectAdapterProvider.JsonTypes => JsonTypes; + public bool CanAdapt(object obj) + { + return obj is IObjectBase; + } + + private Dictionary> JsonTypes => objectTypeListBuilder.JsonTypes; } \ No newline at end of file diff --git a/src/SIL.Harmony/Adapters/IObjectAdapterProvider.cs b/src/SIL.Harmony/Adapters/IObjectAdapterProvider.cs index 6504c0a..98b21dd 100644 --- a/src/SIL.Harmony/Adapters/IObjectAdapterProvider.cs +++ b/src/SIL.Harmony/Adapters/IObjectAdapterProvider.cs @@ -11,6 +11,5 @@ internal interface IObjectAdapterProvider { IEnumerable GetRegistrations(); IObjectBase Adapt(object obj); - - Dictionary> JsonTypes { get; } + bool CanAdapt(object obj); } \ No newline at end of file diff --git a/src/SIL.Harmony/Changes/ChangeContext.cs b/src/SIL.Harmony/Changes/ChangeContext.cs index 15c10d7..bdcf911 100644 --- a/src/SIL.Harmony/Changes/ChangeContext.cs +++ b/src/SIL.Harmony/Changes/ChangeContext.cs @@ -25,5 +25,5 @@ internal ChangeContext(Commit commit, SnapshotWorker worker, CrdtConfig crdtConf } public async ValueTask IsObjectDeleted(Guid entityId) => (await GetSnapshot(entityId))?.EntityIsDeleted ?? true; - internal IObjectBase Adapt(object obj) => _crdtConfig.ObjectTypeListBuilder.AdapterProvider.Adapt(obj); + internal IObjectBase Adapt(object obj) => _crdtConfig.ObjectTypeListBuilder.Adapt(obj); } diff --git a/src/SIL.Harmony/CrdtConfig.cs b/src/SIL.Harmony/CrdtConfig.cs index bcfb6d5..1de1337 100644 --- a/src/SIL.Harmony/CrdtConfig.cs +++ b/src/SIL.Harmony/CrdtConfig.cs @@ -108,8 +108,7 @@ public void Freeze() { if (_frozen) return; _frozen = true; - JsonTypes = AdapterProvider.JsonTypes; - foreach (var registration in AdapterProvider.GetRegistrations()) + foreach (var registration in AdapterProviders.SelectMany(a => a.GetRegistrations())) { ModelConfigurations.Add((builder, config) => { @@ -128,19 +127,18 @@ internal void CheckFrozen() if (_frozen) throw new InvalidOperationException($"{nameof(ObjectTypeListBuilder)} is frozen"); } - internal Dictionary>? JsonTypes { get; set; } + internal Dictionary> JsonTypes { get; } = []; internal List> ModelConfigurations { get; } = []; - internal IObjectAdapterProvider AdapterProvider => _adapterProvider ?? throw new InvalidOperationException("No adapter has been added to the builder"); - private IObjectAdapterProvider? _adapterProvider; + internal List AdapterProviders { get; } = []; public DefaultAdapterProvider DefaultAdapter() { CheckFrozen(); - if (_adapterProvider is not null) throw new InvalidOperationException("adapter has already been added"); - var adapter = new DefaultAdapterProvider(this); - _adapterProvider = adapter; + if (AdapterProviders.OfType().SingleOrDefault() is {} adapter) return adapter; + adapter = new DefaultAdapterProvider(this); + AdapterProviders.Add(adapter); return adapter; } @@ -163,9 +161,26 @@ public CustomAdapterProvider CustomAdapter, IPolyType { CheckFrozen(); - if (_adapterProvider is not null) throw new InvalidOperationException("adapter has already been added"); - var adapter = new CustomAdapterProvider(this); - _adapterProvider = adapter; + if (AdapterProviders.OfType>().SingleOrDefault() is {} adapter) return adapter; + adapter = new CustomAdapterProvider(this); + AdapterProviders.Add(adapter); return adapter; } + + internal IObjectBase Adapt(object obj) + { + if (AdapterProviders is [{ } defaultAdapter]) + { + return defaultAdapter.Adapt(obj); + } + + foreach (var objectAdapterProvider in AdapterProviders) + { + if (objectAdapterProvider.CanAdapt(obj)) + { + return objectAdapterProvider.Adapt(obj); + } + } + throw new ArgumentException($"Unable to adapt object of type {obj.GetType()}"); + } } From d8bcac652734d583d49264bfa7fa07ae8fe2e992 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 22 Oct 2024 15:43:15 +0700 Subject: [PATCH 04/15] ensure Simple kernel can be used across multiple tests with different Json configs used by EF --- src/SIL.Harmony.Sample/CrdtSampleKernel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs index a48720b..a4bd5c3 100644 --- a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs +++ b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs @@ -25,6 +25,9 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi { services.AddDbContext((provider, builder) => { + //this ensures that Ef Conversion methods will not be cached across different IoC containers + //this can show up as the second instance using the JsonSerializerOptions from the first container + builder.UseRootApplicationServiceProvider(); builder.UseLinqToDbCrdt(provider); optionsBuilder(builder); builder.EnableDetailedErrors(); From aab83940604dad909caba1abf35c349db6274420 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 22 Oct 2024 15:43:48 +0700 Subject: [PATCH 05/15] test `BeforePersistObjectInProjectedTable` to store commit id among other data --- src/SIL.Harmony.Tests/DataModelTestBase.cs | 3 +-- .../PersistExtraDataTests.cs | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/SIL.Harmony.Tests/DataModelTestBase.cs b/src/SIL.Harmony.Tests/DataModelTestBase.cs index 23d7805..81db26a 100644 --- a/src/SIL.Harmony.Tests/DataModelTestBase.cs +++ b/src/SIL.Harmony.Tests/DataModelTestBase.cs @@ -37,8 +37,7 @@ public DataModelTestBase(SqliteConnection connection, bool alwaysValidate = true { var serviceCollection = new ServiceCollection() .AddCrdtDataSample(connection) - .AddOptions().Configure(config => config.AlwaysValidateCommits = alwaysValidate) - .Services + .Configure(config => config.AlwaysValidateCommits = alwaysValidate) .Replace(ServiceDescriptor.Singleton(MockTimeProvider)); configure?.Invoke(serviceCollection); _services = serviceCollection.BuildServiceProvider(); diff --git a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs index 0716352..a9e5209 100644 --- a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs +++ b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs @@ -27,7 +27,8 @@ public class ExtraDataModel : IObjectBase public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } public Guid CommitId { get; set; } - public HybridDateTime? HybridDateTime { get; set; } + public DateTimeOffset? DateTime { get; set; } + public long Counter { get; set; } public Guid[] GetReferences() { @@ -40,7 +41,10 @@ public void RemoveReference(Guid id, Commit commit) public IObjectBase Copy() { - return new ExtraDataModel(); + return new ExtraDataModel() + { + Id = Id + }; } } @@ -48,10 +52,20 @@ public PersistExtraDataTests() { _dataModelTestBase = new DataModelTestBase(configure: services => { - services.AddOptions().Configure(config => + services.Configure(config => { config.ObjectTypeListBuilder.DefaultAdapter().Add(); config.ChangeTypeListBuilder.Add(); + config.BeforePersistObjectInProjectedTable = (obj, snapshot) => + { + if (obj is ExtraDataModel extraDataModel) + { + extraDataModel.CommitId = snapshot.CommitId; + extraDataModel.DateTime = snapshot.Commit.HybridDateTime.DateTime; + extraDataModel.Counter = snapshot.Commit.HybridDateTime.Counter; + } + return ValueTask.CompletedTask; + }; }); }); } @@ -64,6 +78,7 @@ public async Task CanPersistExtraData() var extraDataModel = _dataModelTestBase.DataModel.QueryLatest().Should().ContainSingle().Subject; extraDataModel.Id.Should().Be(entityId); extraDataModel.CommitId.Should().Be(commit.Id); - extraDataModel.HybridDateTime.Should().Be(commit.HybridDateTime); + extraDataModel.DateTime.Should().Be(commit.HybridDateTime.DateTime); + extraDataModel.Counter.Should().Be(commit.HybridDateTime.Counter); } } \ No newline at end of file From 731aa554c55a0f0de94cdc608b089fc3131769aa Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 25 Oct 2024 16:02:59 +0700 Subject: [PATCH 06/15] move BeforePersistObject up into the snapshot worker so it's always applied to objects, not only when using projected tables --- src/SIL.Harmony.Tests/PersistExtraDataTests.cs | 8 ++++++-- src/SIL.Harmony/CrdtConfig.cs | 4 ++-- src/SIL.Harmony/Db/CrdtRepository.cs | 2 -- src/SIL.Harmony/SnapshotWorker.cs | 2 ++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs index a9e5209..afb8e9d 100644 --- a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs +++ b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs @@ -43,7 +43,11 @@ public IObjectBase Copy() { return new ExtraDataModel() { - Id = Id + Id = Id, + DeletedAt = DeletedAt, + CommitId = CommitId, + DateTime = DateTime, + Counter = Counter }; } } @@ -56,7 +60,7 @@ public PersistExtraDataTests() { config.ObjectTypeListBuilder.DefaultAdapter().Add(); config.ChangeTypeListBuilder.Add(); - config.BeforePersistObjectInProjectedTable = (obj, snapshot) => + config.BeforePersistObject = (obj, snapshot) => { if (obj is ExtraDataModel extraDataModel) { diff --git a/src/SIL.Harmony/CrdtConfig.cs b/src/SIL.Harmony/CrdtConfig.cs index 1de1337..24c8e2b 100644 --- a/src/SIL.Harmony/CrdtConfig.cs +++ b/src/SIL.Harmony/CrdtConfig.cs @@ -8,7 +8,7 @@ using SIL.Harmony.Entities; namespace SIL.Harmony; -public delegate ValueTask BeforeSaveObject(object obj, ObjectSnapshot snapshot); +public delegate ValueTask BeforeSaveObjectDelegate(object obj, ObjectSnapshot snapshot); public class CrdtConfig { /// @@ -16,7 +16,7 @@ public class CrdtConfig /// it does however increase database size as now objects are stored both in snapshots and in their projected tables /// public bool EnableProjectedTables { get; set; } = true; - public BeforeSaveObject? BeforePersistObjectInProjectedTable { get; set; } + public BeforeSaveObjectDelegate BeforePersistObject { get; set; } = (o, snapshot) => ValueTask.CompletedTask; /// /// after adding any commit validate the commit history, not great for performance but good for testing. /// diff --git a/src/SIL.Harmony/Db/CrdtRepository.cs b/src/SIL.Harmony/Db/CrdtRepository.cs index 383e426..a6ed5e1 100644 --- a/src/SIL.Harmony/Db/CrdtRepository.cs +++ b/src/SIL.Harmony/Db/CrdtRepository.cs @@ -235,7 +235,6 @@ private async ValueTask SnapshotAdded(ObjectSnapshot objectSnapshot) //if we don't make a copy first then the entity will be tracked by the context and be modified //by future changes in the same session entity = objectSnapshot.Entity.Copy().DbObject; - crdtConfig.Value.BeforePersistObjectInProjectedTable?.Invoke(entity, objectSnapshot); _dbContext.Add(entity) .Property(ObjectSnapshot.ShadowRefName).CurrentValue = objectSnapshot.Id; return; @@ -249,7 +248,6 @@ private async ValueTask SnapshotAdded(ObjectSnapshot objectSnapshot) } entity = objectSnapshot.Entity.DbObject; - crdtConfig.Value.BeforePersistObjectInProjectedTable?.Invoke(entity, objectSnapshot); existingEntry.CurrentValues.SetValues(entity); existingEntry.Property(ObjectSnapshot.ShadowRefName).CurrentValue = objectSnapshot.Id; } diff --git a/src/SIL.Harmony/SnapshotWorker.cs b/src/SIL.Harmony/SnapshotWorker.cs index 10f8e69..5b0b649 100644 --- a/src/SIL.Harmony/SnapshotWorker.cs +++ b/src/SIL.Harmony/SnapshotWorker.cs @@ -125,6 +125,8 @@ private async ValueTask ApplyCommitChanges(IEnumerable commits, bool upd { intermediateSnapshots[prevSnapshot.Entity.Id] = prevSnapshot; } + + await _crdtConfig.BeforePersistObject.Invoke(entity.DbObject, newSnapshot); AddSnapshot(newSnapshot); } From 53e763fa9251ea0c06fc4fc6e33511e313426b56 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 25 Oct 2024 16:19:00 +0700 Subject: [PATCH 07/15] fix EF caching issue by actually disabling the cache instead of a different workaround --- src/SIL.Harmony.Sample/CrdtSampleKernel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs index a4bd5c3..527afd0 100644 --- a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs +++ b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs @@ -27,7 +27,8 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi { //this ensures that Ef Conversion methods will not be cached across different IoC containers //this can show up as the second instance using the JsonSerializerOptions from the first container - builder.UseRootApplicationServiceProvider(); + //only needed for testing scenarios + builder.EnableServiceProviderCaching(false); builder.UseLinqToDbCrdt(provider); optionsBuilder(builder); builder.EnableDetailedErrors(); From f0d9689ae8e8eed9a27f825c7aa33bcd45d9d72f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 10:42:12 +0700 Subject: [PATCH 08/15] allow specifying to CrdtSampleKernel that this is a performance test to enable EF Service provider caching as that makes our performance degrade enough to fail the tests --- src/SIL.Harmony.Sample/CrdtSampleKernel.cs | 8 ++------ .../DataModelPerformanceTests.cs | 7 +++++-- src/SIL.Harmony.Tests/DataModelTestBase.cs | 17 +++++++++++------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs index 527afd0..5bf8061 100644 --- a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs +++ b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs @@ -15,20 +15,16 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi { return services.AddCrdtDataSample(builder => builder.UseSqlite($"Data Source={dbPath}")); } - public static IServiceCollection AddCrdtDataSample(this IServiceCollection services, DbConnection connection) - { - return services.AddCrdtDataSample(builder => builder.UseSqlite(connection, true)); - } public static IServiceCollection AddCrdtDataSample(this IServiceCollection services, - Action optionsBuilder) + Action optionsBuilder, bool performanceTest = false) { services.AddDbContext((provider, builder) => { //this ensures that Ef Conversion methods will not be cached across different IoC containers //this can show up as the second instance using the JsonSerializerOptions from the first container //only needed for testing scenarios - builder.EnableServiceProviderCaching(false); + builder.EnableServiceProviderCaching(performanceTest); builder.UseLinqToDbCrdt(provider); optionsBuilder(builder); builder.EnableDetailedErrors(); diff --git a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index d009691..48073d5 100644 --- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs @@ -23,6 +23,9 @@ public class DataModelPerformanceTests(ITestOutputHelper output) [Fact] public void AddingChangePerformance() { + #if DEBUG + Assert.Fail("This test is disabled in debug builds, not reliable"); + #endif var summary = BenchmarkRunner.Run( ManualConfig.CreateEmpty() @@ -188,7 +191,7 @@ public class DataModelPerformanceBenchmarks [GlobalSetup] public void GlobalSetup() { - _templateModel = new DataModelTestBase(alwaysValidate: false); + _templateModel = new DataModelTestBase(alwaysValidate: false, performanceTest: true); DataModelPerformanceTests.BulkInsertChanges(_templateModel, StartingSnapshots).GetAwaiter().GetResult(); } @@ -198,7 +201,7 @@ public void GlobalSetup() [IterationSetup] public void IterationSetup() { - _emptyDataModel = new(alwaysValidate: false); + _emptyDataModel = new(alwaysValidate: false, performanceTest: true); _ = _emptyDataModel.WriteNextChange(_emptyDataModel.SetWord(Guid.NewGuid(), "entity1")).Result; _dataModelTestBase = _templateModel.ForkDatabase(false); } diff --git a/src/SIL.Harmony.Tests/DataModelTestBase.cs b/src/SIL.Harmony.Tests/DataModelTestBase.cs index 81db26a..0b2515c 100644 --- a/src/SIL.Harmony.Tests/DataModelTestBase.cs +++ b/src/SIL.Harmony.Tests/DataModelTestBase.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using Microsoft.Data.Sqlite; using SIL.Harmony.Changes; using SIL.Harmony.Core; @@ -17,15 +18,16 @@ public class DataModelTestBase : IAsyncLifetime { protected readonly ServiceProvider _services; protected readonly Guid _localClientId = Guid.NewGuid(); + private readonly bool _performanceTest; public readonly DataModel DataModel; public readonly SampleDbContext DbContext; internal readonly CrdtRepository CrdtRepository; protected readonly MockTimeProvider MockTimeProvider = new(); public DataModelTestBase(bool saveToDisk = false, bool alwaysValidate = true, - Action? configure = null) : this(saveToDisk + Action? configure = null, bool performanceTest = false) : this(saveToDisk ? new SqliteConnection("Data Source=test.db") - : new SqliteConnection("Data Source=:memory:"), alwaysValidate, configure) + : new SqliteConnection("Data Source=:memory:"), alwaysValidate, configure, performanceTest) { } @@ -33,10 +35,13 @@ public DataModelTestBase() : this(new SqliteConnection("Data Source=:memory:")) { } - public DataModelTestBase(SqliteConnection connection, bool alwaysValidate = true, Action? configure = null) + public DataModelTestBase(SqliteConnection connection, bool alwaysValidate = true, Action? configure = null, bool performanceTest = false) { - var serviceCollection = new ServiceCollection() - .AddCrdtDataSample(connection) + _performanceTest = performanceTest; + var serviceCollection = new ServiceCollection().AddCrdtDataSample(builder => + { + builder.UseSqlite(connection, true); + }, performanceTest) .Configure(config => config.AlwaysValidateCommits = alwaysValidate) .Replace(ServiceDescriptor.Singleton(MockTimeProvider)); configure?.Invoke(serviceCollection); @@ -55,7 +60,7 @@ public DataModelTestBase ForkDatabase(bool alwaysValidate = true) var existingConnection = DbContext.Database.GetDbConnection() as SqliteConnection; if (existingConnection is null) throw new InvalidOperationException("Database is not SQLite"); existingConnection.BackupDatabase(connection); - var newTestBase = new DataModelTestBase(connection, alwaysValidate); + var newTestBase = new DataModelTestBase(connection, alwaysValidate, performanceTest: _performanceTest); newTestBase.SetCurrentDate(currentDate.DateTime); return newTestBase; } From 6e268c9199d0609350c7652ba38b7e26ba8a3fe7 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 11:11:35 +0700 Subject: [PATCH 09/15] enable benchmarking comments --- .github/workflows/test.yaml | 18 +++++++++++++++++- .../DataModelPerformanceTests.cs | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 65494e2..8fbc8fb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,4 +19,20 @@ jobs: uses: actions/setup-dotnet@v4 - name: Build & test - run: dotnet test --configuration Release --logger GitHubActions \ No newline at end of file + run: dotnet test --configuration Release --logger GitHubActions + - name: Download previous benchmark data + uses: actions/cache@v4 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + - name: Continuous Benchmark + uses: benchmark-action/github-action-benchmark@v1.20.4 + with: + tool: benchmarkdotnet + output-file-path: src\artifacts\bin\SIL.Harmony.Tests\release\BenchmarkDotNet.Artifacts\results\SIL.Harmony.Tests.DataModelPerformanceBenchmarks-report-full-compressed.json + external-data-json-path: ./cache/benchmark-data.json + fail-on-alert: true + summary-always: true + comment-always: true + github-token: ${{ secrets.GITHUB_TOKEN }} + \ No newline at end of file diff --git a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index 48073d5..0674aaf 100644 --- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs @@ -29,6 +29,7 @@ public void AddingChangePerformance() var summary = BenchmarkRunner.Run( ManualConfig.CreateEmpty() + .AddExporter(JsonExporter.FullCompressed) .AddColumnProvider(DefaultColumnProviders.Instance) .AddLogger(new XUnitBenchmarkLogger(output)) ); From 3e2ee34213008b0d906a51f7f4bdb5053f1b29db Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 11:14:59 +0700 Subject: [PATCH 10/15] fix permissions and import --- .github/workflows/test.yaml | 3 ++- src/SIL.Harmony.Tests/DataModelPerformanceTests.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8fbc8fb..0ff6569 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,7 +6,8 @@ on: pull_request: branches: - main - +permissions: + pull-requests: write #allow benchmark-action to comment on PRs jobs: build: runs-on: ubuntu-latest diff --git a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index 0674aaf..b5b4443 100644 --- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Exporters.Json; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; using JetBrains.Profiler.SelfApi; From 1f46e1bac5041cbf986514495ed6855bd7ccc944 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 11:23:20 +0700 Subject: [PATCH 11/15] fix path for benchmark output file --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0ff6569..352dfb8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -30,7 +30,7 @@ jobs: uses: benchmark-action/github-action-benchmark@v1.20.4 with: tool: benchmarkdotnet - output-file-path: src\artifacts\bin\SIL.Harmony.Tests\release\BenchmarkDotNet.Artifacts\results\SIL.Harmony.Tests.DataModelPerformanceBenchmarks-report-full-compressed.json + output-file-path: src/artifacts/bin/SIL.Harmony.Tests/release/BenchmarkDotNet.Artifacts/results/SIL.Harmony.Tests.DataModelPerformanceBenchmarks-report-full-compressed.json external-data-json-path: ./cache/benchmark-data.json fail-on-alert: true summary-always: true From c5bbd2b3c973091c4ce20db35bab0c1a9ec1f4e9 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 11:49:33 +0700 Subject: [PATCH 12/15] manually degrade performance to test new benchmark CI --- src/SIL.Harmony.Tests/DataModelPerformanceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index b5b4443..6c8d5fa 100644 --- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs @@ -182,7 +182,7 @@ public void Flush() // disable warning about waiting for sync code, benchmarkdotnet does not support async code, and it doesn't deadlock when waiting. #pragma warning disable VSTHRD002 -[SimpleJob(RunStrategy.Throughput, warmupCount: 2)] +[SimpleJob(RunStrategy.Monitoring, warmupCount: 2)] public class DataModelPerformanceBenchmarks { private DataModelTestBase _templateModel = null!; @@ -203,7 +203,7 @@ public void GlobalSetup() [IterationSetup] public void IterationSetup() { - _emptyDataModel = new(alwaysValidate: false, performanceTest: true); + _emptyDataModel = new(alwaysValidate: false, performanceTest: false); _ = _emptyDataModel.WriteNextChange(_emptyDataModel.SetWord(Guid.NewGuid(), "entity1")).Result; _dataModelTestBase = _templateModel.ForkDatabase(false); } From b1a1041cae0f5d8607ed924152434ce559b86b3e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 11:57:39 +0700 Subject: [PATCH 13/15] only comment when there's an alert, make performance good again --- .github/workflows/test.yaml | 3 +-- src/SIL.Harmony.Tests/DataModelPerformanceTests.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 352dfb8..e13030e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -33,7 +33,6 @@ jobs: output-file-path: src/artifacts/bin/SIL.Harmony.Tests/release/BenchmarkDotNet.Artifacts/results/SIL.Harmony.Tests.DataModelPerformanceBenchmarks-report-full-compressed.json external-data-json-path: ./cache/benchmark-data.json fail-on-alert: true - summary-always: true - comment-always: true + comment-on-alert: true github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index 6c8d5fa..e53bb92 100644 --- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs @@ -203,7 +203,7 @@ public void GlobalSetup() [IterationSetup] public void IterationSetup() { - _emptyDataModel = new(alwaysValidate: false, performanceTest: false); + _emptyDataModel = new(alwaysValidate: false, performanceTest: true); _ = _emptyDataModel.WriteNextChange(_emptyDataModel.SetWord(Guid.NewGuid(), "entity1")).Result; _dataModelTestBase = _templateModel.ForkDatabase(false); } From 948fcf09ef2680c9090c2e42ed7c14a0348a01dc Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 12:08:53 +0700 Subject: [PATCH 14/15] change run strategy back to Throughput like it should have been --- src/SIL.Harmony.Tests/DataModelPerformanceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index e53bb92..b5b4443 100644 --- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs @@ -182,7 +182,7 @@ public void Flush() // disable warning about waiting for sync code, benchmarkdotnet does not support async code, and it doesn't deadlock when waiting. #pragma warning disable VSTHRD002 -[SimpleJob(RunStrategy.Monitoring, warmupCount: 2)] +[SimpleJob(RunStrategy.Throughput, warmupCount: 2)] public class DataModelPerformanceBenchmarks { private DataModelTestBase _templateModel = null!; From 965c83696cdac180838ff5423fe503de6c4a3d21 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 13:05:59 +0700 Subject: [PATCH 15/15] change BeforePersistObject to BeforeSaveObject --- src/SIL.Harmony.Tests/PersistExtraDataTests.cs | 2 +- src/SIL.Harmony/CrdtConfig.cs | 2 +- src/SIL.Harmony/SnapshotWorker.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs index afb8e9d..3fadfaf 100644 --- a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs +++ b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs @@ -60,7 +60,7 @@ public PersistExtraDataTests() { config.ObjectTypeListBuilder.DefaultAdapter().Add(); config.ChangeTypeListBuilder.Add(); - config.BeforePersistObject = (obj, snapshot) => + config.BeforeSaveObject = (obj, snapshot) => { if (obj is ExtraDataModel extraDataModel) { diff --git a/src/SIL.Harmony/CrdtConfig.cs b/src/SIL.Harmony/CrdtConfig.cs index 24c8e2b..6c16337 100644 --- a/src/SIL.Harmony/CrdtConfig.cs +++ b/src/SIL.Harmony/CrdtConfig.cs @@ -16,7 +16,7 @@ public class CrdtConfig /// it does however increase database size as now objects are stored both in snapshots and in their projected tables /// public bool EnableProjectedTables { get; set; } = true; - public BeforeSaveObjectDelegate BeforePersistObject { get; set; } = (o, snapshot) => ValueTask.CompletedTask; + public BeforeSaveObjectDelegate BeforeSaveObject { get; set; } = (o, snapshot) => ValueTask.CompletedTask; /// /// after adding any commit validate the commit history, not great for performance but good for testing. /// diff --git a/src/SIL.Harmony/SnapshotWorker.cs b/src/SIL.Harmony/SnapshotWorker.cs index 5b0b649..5034804 100644 --- a/src/SIL.Harmony/SnapshotWorker.cs +++ b/src/SIL.Harmony/SnapshotWorker.cs @@ -126,7 +126,7 @@ private async ValueTask ApplyCommitChanges(IEnumerable commits, bool upd intermediateSnapshots[prevSnapshot.Entity.Id] = prevSnapshot; } - await _crdtConfig.BeforePersistObject.Invoke(entity.DbObject, newSnapshot); + await _crdtConfig.BeforeSaveObject.Invoke(entity.DbObject, newSnapshot); AddSnapshot(newSnapshot); }