From c7dea1f1511da52b7c130b8d2e8bc652941e5dd1 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 13:31:13 +0700 Subject: [PATCH] Allow storing extra data on objects before persisting (#17) * rename GetLatestObjects to QueryLatest * allow multiple object adapters * ensure Simple kernel can be used across multiple tests with different Json configs used by EF * test `BeforeSaveObject ` to store commit id among other data * move BeforeSaveObject up into the snapshot worker so it's always applied to objects, not only when using projected tables * enable benchmarking comments * change BeforePersistObject to BeforeSaveObject --- .github/workflows/test.yaml | 20 ++++- src/SIL.Harmony.Sample/CrdtSampleKernel.cs | 10 +-- .../Adapter/CustomObjectAdapterTests.cs | 2 +- .../DataModelPerformanceTests.cs | 9 +- .../DataModelSimpleChanges.cs | 2 +- src/SIL.Harmony.Tests/DataModelTestBase.cs | 26 +++--- src/SIL.Harmony.Tests/DataQueryTests.cs | 2 +- src/SIL.Harmony.Tests/DefinitionTests.cs | 8 +- .../PersistExtraDataTests.cs | 88 +++++++++++++++++++ src/SIL.Harmony.Tests/SyncTests.cs | 4 +- .../Adapters/CustomAdapterProvider.cs | 8 +- .../Adapters/DefaultAdapterProvider.cs | 8 +- .../Adapters/IObjectAdapterProvider.cs | 3 +- src/SIL.Harmony/Changes/ChangeContext.cs | 2 +- src/SIL.Harmony/CrdtConfig.cs | 40 ++++++--- src/SIL.Harmony/DataModel.cs | 2 +- src/SIL.Harmony/Db/CrdtRepository.cs | 7 +- src/SIL.Harmony/SnapshotWorker.cs | 2 + 18 files changed, 193 insertions(+), 50 deletions(-) create mode 100644 src/SIL.Harmony.Tests/PersistExtraDataTests.cs diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 65494e2..e13030e 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 @@ -19,4 +20,19 @@ 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 + comment-on-alert: true + github-token: ${{ secrets.GITHUB_TOKEN }} + \ No newline at end of file diff --git a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs index a48720b..5bf8061 100644 --- a/src/SIL.Harmony.Sample/CrdtSampleKernel.cs +++ b/src/SIL.Harmony.Sample/CrdtSampleKernel.cs @@ -15,16 +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(performanceTest); builder.UseLinqToDbCrdt(provider); optionsBuilder(builder); builder.EnableDetailedErrors(); 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/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index d009691..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; @@ -23,9 +24,13 @@ 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() + .AddExporter(JsonExporter.FullCompressed) .AddColumnProvider(DefaultColumnProviders.Instance) .AddLogger(new XUnitBenchmarkLogger(output)) ); @@ -188,7 +193,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 +203,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/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/DataModelTestBase.cs b/src/SIL.Harmony.Tests/DataModelTestBase.cs index de2329b..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,14 +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) : this(saveToDisk + public DataModelTestBase(bool saveToDisk = false, bool alwaysValidate = true, + Action? configure = null, bool performanceTest = false) : this(saveToDisk ? new SqliteConnection("Data Source=test.db") - : new SqliteConnection("Data Source=:memory:"), alwaysValidate) + : new SqliteConnection("Data Source=:memory:"), alwaysValidate, configure, performanceTest) { } @@ -32,14 +35,17 @@ 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, bool performanceTest = false) { - _services = new ServiceCollection() - .AddCrdtDataSample(connection) - .AddOptions().Configure(config => config.AlwaysValidateCommits = alwaysValidate) - .Services - .Replace(ServiceDescriptor.Singleton(MockTimeProvider)) - .BuildServiceProvider(); + _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); + _services = serviceCollection.BuildServiceProvider(); DbContext = _services.GetRequiredService(); DbContext.Database.OpenConnection(); DbContext.Database.EnsureCreated(); @@ -54,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; } 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/PersistExtraDataTests.cs b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs new file mode 100644 index 0000000..3fadfaf --- /dev/null +++ b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs @@ -0,0 +1,88 @@ +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 DateTimeOffset? DateTime { get; set; } + public long Counter { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, Commit commit) + { + } + + public IObjectBase Copy() + { + return new ExtraDataModel() + { + Id = Id, + DeletedAt = DeletedAt, + CommitId = CommitId, + DateTime = DateTime, + Counter = Counter + }; + } + } + + public PersistExtraDataTests() + { + _dataModelTestBase = new DataModelTestBase(configure: services => + { + services.Configure(config => + { + config.ObjectTypeListBuilder.DefaultAdapter().Add(); + config.ChangeTypeListBuilder.Add(); + config.BeforeSaveObject = (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; + }; + }); + }); + } + + [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.DateTime.Should().Be(commit.HybridDateTime.DateTime); + extraDataModel.Counter.Should().Be(commit.HybridDateTime.Counter); + } +} \ No newline at end of file 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/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 1b9837a..6c16337 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 BeforeSaveObjectDelegate(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 BeforeSaveObjectDelegate BeforeSaveObject { get; set; } = (o, snapshot) => ValueTask.CompletedTask; /// /// after adding any commit validate the commit history, not great for performance but good for testing. /// @@ -107,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) => { @@ -127,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; } @@ -162,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()}"); + } } 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) diff --git a/src/SIL.Harmony/Db/CrdtRepository.cs b/src/SIL.Harmony/Db/CrdtRepository.cs index bb94e70..a6ed5e1 100644 --- a/src/SIL.Harmony/Db/CrdtRepository.cs +++ b/src/SIL.Harmony/Db/CrdtRepository.cs @@ -229,11 +229,13 @@ 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; + _dbContext.Add(entity) .Property(ObjectSnapshot.ShadowRefName).CurrentValue = objectSnapshot.Id; return; } @@ -245,7 +247,8 @@ private async ValueTask SnapshotAdded(ObjectSnapshot objectSnapshot) return; } - existingEntry.CurrentValues.SetValues(objectSnapshot.Entity.DbObject); + entity = objectSnapshot.Entity.DbObject; + 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..5034804 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.BeforeSaveObject.Invoke(entity.DbObject, newSnapshot); AddSnapshot(newSnapshot); }