diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4263bab..a7a4243 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,26 +3,29 @@
true
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 9f63e20..5e9eda0 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,6 +1,6 @@
- net8.0
+ net10.0
enable
false
enable
diff --git a/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs b/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs
index 5c32565..a4d9f8f 100644
--- a/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs
+++ b/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs
@@ -1,6 +1,6 @@
using SIL.Harmony.Core;
-using LinqToDB.AspNet.Logging;
using LinqToDB.EntityFrameworkCore;
+using LinqToDB.Extensions.Logging;
using LinqToDB.Mapping;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
diff --git a/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj b/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj
index d530422..1453659 100644
--- a/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj
+++ b/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj
@@ -10,8 +10,8 @@
-
+
diff --git a/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs b/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs
index 8014d54..0be8f90 100644
--- a/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs
+++ b/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs
@@ -158,8 +158,8 @@ public async Task CanAdaptACustomObject()
.Add(builder => builder.HasKey(o => o.Identifier));
}).BuildServiceProvider();
var myDbContext = services.GetRequiredService();
- await myDbContext.Database.OpenConnectionAsync();
- await myDbContext.Database.EnsureCreatedAsync();
+ await myDbContext.Database.OpenConnectionAsync(TestContext.Current.CancellationToken);
+ await myDbContext.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken);
var dataModel = services.GetRequiredService();
var objectId = Guid.NewGuid();
var objectId2 = Guid.NewGuid();
@@ -194,6 +194,6 @@ await dataModel.AddChange(Guid.NewGuid(),
myClass2.MyNumber.Should().Be(123.45m);
myClass2.DeletedTime.Should().BeNull();
- dataModel.QueryLatest().ToBlockingEnumerable().Should().NotBeEmpty();
+ dataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).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 84398e5..f3ba500 100644
--- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs
+++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs
@@ -11,7 +11,6 @@
using SIL.Harmony.Changes;
using SIL.Harmony.Db;
using SIL.Harmony.Sample.Changes;
-using Xunit.Abstractions;
namespace SIL.Harmony.Tests;
diff --git a/src/SIL.Harmony.Tests/DataModelReferenceTests.cs b/src/SIL.Harmony.Tests/DataModelReferenceTests.cs
index cad2545..7d77c4f 100644
--- a/src/SIL.Harmony.Tests/DataModelReferenceTests.cs
+++ b/src/SIL.Harmony.Tests/DataModelReferenceTests.cs
@@ -10,7 +10,7 @@ public class DataModelReferenceTests : DataModelTestBase
private readonly Guid _word1Id = Guid.NewGuid();
private readonly Guid _word2Id = Guid.NewGuid();
- public override async Task InitializeAsync()
+ public override async ValueTask InitializeAsync()
{
await base.InitializeAsync();
await WriteNextChange(SetWord(_word1Id, "entity1"));
@@ -41,7 +41,7 @@ public async Task AddReferenceWorks(bool includeObjectInSnapshot)
// assert - projected entity
var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym))
- .Where(w => w.Id == _word1Id).SingleOrDefaultAsync();
+ .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.AntonymId.Should().Be(_word2Id);
entityWord.Antonym.Should().NotBeNull();
@@ -80,7 +80,7 @@ await WriteNextChange(
// assert - projected entity
var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym))
- .Where(w => w.Id == word3Id).SingleOrDefaultAsync();
+ .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.AntonymId.Should().Be(_word2Id);
entityWord.Antonym.Should().NotBeNull();
@@ -118,7 +118,7 @@ await WriteNextChange(new SetAntonymReferenceChange(word3Id, _word2Id, setObject
// assert - projected entity
var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym))
- .Where(w => w.Id == word3Id).SingleOrDefaultAsync();
+ .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.AntonymId.Should().Be(_word2Id);
entityWord.Antonym.Should().NotBeNull();
@@ -157,7 +157,7 @@ await WriteNextChange(
// assert - projected entity
var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym))
- .Where(w => w.Id == word3Id).SingleOrDefaultAsync();
+ .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.Text.Should().Be("entity3");
entityWord.AntonymId.Should().Be(_word1Id);
@@ -197,7 +197,7 @@ await WriteNextChange(
// assert - projected entity
var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym))
- .Where(w => w.Id == _word1Id).SingleOrDefaultAsync();
+ .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.Text.Should().Be("entity1");
entityWord.AntonymId.Should().Be(word3Id);
@@ -236,7 +236,7 @@ await WriteNextChange(new SetAntonymReferenceChange(word3Id, _word1Id, setObject
// assert - projected entity
var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym))
- .Where(w => w.Id == word3Id).SingleOrDefaultAsync();
+ .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.Text.Should().Be("entity3");
entityWord.AntonymId.Should().Be(_word1Id);
@@ -275,7 +275,7 @@ await WriteNextChange(new SetAntonymReferenceChange(_word1Id, word3Id, setObject
// assert - projected entity
var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym))
- .Where(w => w.Id == _word1Id).SingleOrDefaultAsync();
+ .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.Text.Should().Be("entity1");
entityWord.AntonymId.Should().Be(word3Id);
@@ -341,10 +341,10 @@ await AddCommitsViaSync([
initialWordCommit,
deleteWordCommit
]);
- var snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == initialWordCommit.Id);
+ var snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == initialWordCommit.Id, TestContext.Current.CancellationToken);
var initialWord = (Word) snapshot.Entity;
initialWord.AntonymId.Should().Be(_word1Id);
- snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == deleteWordCommit.Id && s.EntityId == wordId);
+ snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == deleteWordCommit.Id && s.EntityId == wordId, TestContext.Current.CancellationToken);
var wordWithoutRef = (Word) snapshot.Entity;
wordWithoutRef.AntonymId.Should().BeNull();
}
@@ -422,7 +422,7 @@ public async Task CanCreate2TagsWithTheSameNameOutOfOrder()
var commitA = await WriteNextChange(SetTag(Guid.NewGuid(), tagText));
//represents someone syncing in a tag with the same name
await WriteChangeBefore(commitA, SetTag(Guid.NewGuid(), tagText));
- DataModel.QueryLatest().ToBlockingEnumerable().Where(t => t.Text == tagText).Should().ContainSingle();
+ DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Where(t => t.Text == tagText).Should().ContainSingle();
}
[Fact]
@@ -434,6 +434,6 @@ public async Task CanUpdateTagWithTheSameNameOutOfOrder()
var commitA = await WriteNextChange(SetTag(Guid.NewGuid(), tagText));
//represents someone syncing in a tag with the same name
await WriteNextChange(SetTag(renameTagId, tagText));
- DataModel.QueryLatest().ToBlockingEnumerable().Where(t => t.Text == tagText).Should().ContainSingle();
+ DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Where(t => t.Text == tagText).Should().ContainSingle();
}
}
diff --git a/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs b/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs
index 7d989c1..8d92cc1 100644
--- a/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs
+++ b/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs
@@ -77,7 +77,7 @@ public async Task WriteMultipleCommits()
await WriteNextChange(SetWord(Guid.NewGuid(), "change 3"));
DbContext.Snapshots.Should().HaveCount(3);
- DataModel.QueryLatest().ToBlockingEnumerable().Should().HaveCount(3);
+ DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Should().HaveCount(3);
}
[Fact]
@@ -95,7 +95,7 @@ public async Task Writing2ChangesSecondOverwritesFirst()
{
await WriteNextChange(SetWord(_entity1Id, "first"));
await WriteNextChange(SetWord(_entity1Id, "second"));
- var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
+ var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken);
snapshot.Entity.Is().Text.Should().Be("second");
}
@@ -107,7 +107,7 @@ public async Task CanWriteChangesWhenAnUnchangedWordExists()
await WriteNextChange(SetWord(_entity1Id, "word-1"));
await WriteNextChange(SetWord(_entity1Id, "second"));
await WriteNextChange(SetWord(_entity1Id, "third"));
- var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
+ var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken);
snapshot.Entity.Is().Text.Should().Be("third");
}
@@ -118,7 +118,7 @@ public async Task Writing2ChangesSecondOverwritesFirstWithLocalFirst()
var secondDate = DateTimeOffset.UtcNow.AddSeconds(1);
await WriteChange(_localClientId, firstDate, SetWord(_entity1Id, "first"));
await WriteChange(_localClientId, secondDate, SetWord(_entity1Id, "second"));
- var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
+ var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken);
snapshot.Entity.Is().Text.Should().Be("second");
}
@@ -129,7 +129,7 @@ public async Task Writing2ChangesSecondOverwritesFirstWithUtcFirst()
var secondDate = DateTimeOffset.Now.AddSeconds(1);
await WriteChange(_localClientId, firstDate, SetWord(_entity1Id, "first"));
await WriteChange(_localClientId, secondDate, SetWord(_entity1Id, "second"));
- var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
+ var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken);
snapshot.Entity.Is().Text.Should().Be("second");
}
@@ -185,7 +185,7 @@ public async Task CanDeleteAnEntry()
{
await WriteNextChange(SetWord(_entity1Id, "test-value"));
var deleteCommit = await WriteNextChange(new DeleteChange(_entity1Id));
- var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
+ var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken);
snapshot.Entity.DeletedAt.Should().Be(deleteCommit.DateTime);
}
@@ -198,7 +198,7 @@ public async Task ApplyChange_ModifiesADeletedEntry()
newNoteChange.SupportsApplyChange().Should().BeTrue();
newNoteChange.SupportsNewEntity().Should().BeFalse(); // otherwise it will override the delete
await WriteNextChange(newNoteChange);
- var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
+ var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken);
var word = snapshot.Entity.Is();
word.Text.Should().Be("test-value");
word.Note.Should().Be("note-after-delete");
@@ -213,7 +213,7 @@ public async Task NewEntity_UndeletesAnEntry()
var recreateChange = SetWord(_entity1Id, "after-undo-delete");
recreateChange.SupportsNewEntity().Should().BeTrue();
await WriteNextChange(recreateChange);
- var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
+ var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken);
var word = snapshot.Entity.Is();
word.Text.Should().Be("after-undo-delete");
word.DeletedAt.Should().Be(null);
diff --git a/src/SIL.Harmony.Tests/DataModelTestBase.cs b/src/SIL.Harmony.Tests/DataModelTestBase.cs
index 09e5b2c..d195249 100644
--- a/src/SIL.Harmony.Tests/DataModelTestBase.cs
+++ b/src/SIL.Harmony.Tests/DataModelTestBase.cs
@@ -161,12 +161,12 @@ public IChange NewDefinition(Guid wordId,
};
}
- public virtual Task InitializeAsync()
+ public virtual ValueTask InitializeAsync()
{
- return Task.CompletedTask;
+ return ValueTask.CompletedTask;
}
- public async Task DisposeAsync()
+ public async ValueTask DisposeAsync()
{
await _services.DisposeAsync();
diff --git a/src/SIL.Harmony.Tests/DataQueryTests.cs b/src/SIL.Harmony.Tests/DataQueryTests.cs
index 1144e4b..37705b2 100644
--- a/src/SIL.Harmony.Tests/DataQueryTests.cs
+++ b/src/SIL.Harmony.Tests/DataQueryTests.cs
@@ -6,7 +6,7 @@ namespace SIL.Harmony.Tests;
public class DataQueryTests: DataModelTestBase
{
private readonly Guid _entity1Id = Guid.NewGuid();
- public override async Task InitializeAsync()
+ public override async ValueTask InitializeAsync()
{
await base.InitializeAsync();
await WriteNextChange(SetWord(_entity1Id, "entity1"));
@@ -15,7 +15,7 @@ public override async Task InitializeAsync()
[Fact]
public async Task CanQueryLatestData()
{
- var entries = await DataModel.QueryLatest().ToArrayAsync();
+ var entries = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken);
var entry = entries.Should().ContainSingle().Subject;
entry.Text.Should().Be("entity1");
}
diff --git a/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt b/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt
index 79ccc2f..d02fa1a 100644
--- a/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt
+++ b/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt
@@ -24,7 +24,7 @@
Keys:
Id PK
Annotations:
- DiscriminatorProperty:
+ CustomIndex:CompositeIndexes: [{"paths":["HybridDateTime.DateTime","HybridDateTime.Counter","Id"],"unique":false,"name":"IX_Commits_DateTime_Counter_Id"}]
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -44,7 +44,6 @@
Foreign keys:
ChangeEntity {'CommitId'} -> Commit {'Id'} Required Cascade ToDependent: ChangeEntities
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -66,7 +65,7 @@
ElementType: Element type: Guid Required
TypeName (string) Required
Navigations:
- Commit (Commit) ToPrincipal Commit Inverse: Snapshots
+ Commit (Commit) Required ToPrincipal Commit Inverse: Snapshots
Keys:
Id PK
Foreign keys:
@@ -75,7 +74,6 @@
EntityId
CommitId, EntityId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -89,7 +87,6 @@
Keys:
Id PK
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -109,7 +106,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -135,7 +131,6 @@
SnapshotId Unique
WordId
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -156,7 +151,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -179,7 +173,6 @@
SnapshotId Unique
Text Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -196,7 +189,7 @@
SnapshotId (no field, Guid?) Shadow FK Index
Text (string) Required
Navigations:
- Antonym (Word) ToPrincipal Word
+ Antonym (Word) Optional ToPrincipal Word
Skip navigations:
Tags (List) CollectionTag Inverse: Word
Keys:
@@ -208,7 +201,6 @@
AntonymId
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -233,7 +225,6 @@
TagId
WordId, TagId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -241,4 +232,4 @@
Relational:ViewName:
Relational:ViewSchema:
Annotations:
- ProductVersion: 9.0.4
\ No newline at end of file
+ ProductVersion: 10.0.7
\ No newline at end of file
diff --git a/src/SIL.Harmony.Tests/DbContextTests.cs b/src/SIL.Harmony.Tests/DbContextTests.cs
index 2cc91ac..3b7677d 100644
--- a/src/SIL.Harmony.Tests/DbContextTests.cs
+++ b/src/SIL.Harmony.Tests/DbContextTests.cs
@@ -1,90 +1,90 @@
-using LinqToDB;
-using LinqToDB.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-
-namespace SIL.Harmony.Tests;
-
-public class DbContextTests: DataModelTestBase
-{
- [Fact]
- public async Task VerifyModel()
- {
- await Verify(DbContext.Model.ToDebugString(MetadataDebugStringOptions.LongDefault));
- }
-
- [Theory]
- [InlineData(0)]
- [InlineData(4)]
- [InlineData(-4)]
- public async Task CanRoundTripDatesFromEf(int offset)
- {
- var commitId = Guid.NewGuid();
- var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset));
- var commit = new Commit(commitId)
- {
- ClientId = Guid.NewGuid(),
- HybridDateTime = new HybridDateTime(expectedDateTime, 0)
- };
- DbContext.Add(commit);
- await DbContext.SaveChangesAsync();
- var actualCommit = await DbContext.Commits.AsNoTracking().SingleOrDefaultAsyncEF(c => c.Id == commitId);
- actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF");
- actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId);
- actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB");
- }
-
-
- [Theory]
- [InlineData(0)]
- [InlineData(4)]
- [InlineData(-4)]
- public async Task CanRoundTripDatesFromLinq2Db(int offset)
- {
-
- var commitId = Guid.NewGuid();
- var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset));
-
- await DbContext.Set().ToLinqToDBTable().AsValueInsertable()
- .Value(c => c.Id, commitId)
- .Value(c => c.ClientId, Guid.NewGuid())
- .Value(c => c.HybridDateTime.DateTime, expectedDateTime)
- .Value(c => c.HybridDateTime.Counter, 0)
- .Value(c => c.Metadata, new CommitMetadata())
- .Value(c => c.Hash, "")
- .Value(c => c.ParentHash, "")
- .InsertAsync();
- var actualCommit = await DbContext.Commits.SingleOrDefaultAsyncEF(c => c.Id == commitId);
- actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF");
- actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId);
- actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB");
- }
-
- [Theory]
- [InlineData(TimeSpan.TicksPerHour)]
- [InlineData(TimeSpan.TicksPerMinute)]
- [InlineData(TimeSpan.TicksPerSecond)]
- [InlineData(TimeSpan.TicksPerMillisecond)]
- [InlineData(TimeSpan.TicksPerMicrosecond)]
- [InlineData(1)]
- public async Task CanFilterCommitsByDateTime(double scale)
- {
- var baseDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.Zero);
- for (int i = 0; i < 50; i++)
- {
- var offset = new TimeSpan((long)(i * scale));
- DbContext.Add(new Commit
- {
- ClientId = Guid.NewGuid(),
- HybridDateTime = new HybridDateTime(baseDateTime.Add(offset), 0)
- });
- }
-
- await DbContext.SaveChangesAsync();
- var commits = await DbContext.Commits
- .Where(c => c.HybridDateTime.DateTime > baseDateTime.Add(new TimeSpan((long)(25 * scale))))
- .OrderBy(c => c.HybridDateTime.DateTime)
- .ToArrayAsyncEF();
- commits.Should().HaveCount(24);
- }
-}
+using LinqToDB;
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace SIL.Harmony.Tests;
+
+public class DbContextTests: DataModelTestBase
+{
+ [Fact]
+ public async Task VerifyModel()
+ {
+ await Verify(DbContext.Model.ToDebugString(MetadataDebugStringOptions.LongDefault));
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(4)]
+ [InlineData(-4)]
+ public async Task CanRoundTripDatesFromEf(int offset)
+ {
+ var commitId = Guid.NewGuid();
+ var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset));
+ var commit = new Commit(commitId)
+ {
+ ClientId = Guid.NewGuid(),
+ HybridDateTime = new HybridDateTime(expectedDateTime, 0)
+ };
+ DbContext.Add(commit);
+ await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken);
+ var actualCommit = await DbContext.Commits.AsNoTracking().SingleOrDefaultAsyncEF(c => c.Id == commitId, TestContext.Current.CancellationToken);
+ actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF");
+ actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId, TestContext.Current.CancellationToken);
+ actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB");
+ }
+
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(4)]
+ [InlineData(-4)]
+ public async Task CanRoundTripDatesFromLinq2Db(int offset)
+ {
+
+ var commitId = Guid.NewGuid();
+ var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset));
+
+ await DbContext.Set().ToLinqToDBTable().AsValueInsertable()
+ .Value(c => c.Id, commitId)
+ .Value(c => c.ClientId, Guid.NewGuid())
+ .Value(c => c.HybridDateTime.DateTime, expectedDateTime)
+ .Value(c => c.HybridDateTime.Counter, 0)
+ .Value(c => c.Metadata, new CommitMetadata())
+ .Value(c => c.Hash, "")
+ .Value(c => c.ParentHash, "")
+ .InsertAsync(TestContext.Current.CancellationToken);
+ var actualCommit = await DbContext.Commits.SingleOrDefaultAsyncEF(c => c.Id == commitId, TestContext.Current.CancellationToken);
+ actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF");
+ actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId, TestContext.Current.CancellationToken);
+ actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB");
+ }
+
+ [Theory]
+ [InlineData(TimeSpan.TicksPerHour)]
+ [InlineData(TimeSpan.TicksPerMinute)]
+ [InlineData(TimeSpan.TicksPerSecond)]
+ [InlineData(TimeSpan.TicksPerMillisecond)]
+ [InlineData(TimeSpan.TicksPerMicrosecond)]
+ [InlineData(1)]
+ public async Task CanFilterCommitsByDateTime(double scale)
+ {
+ var baseDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.Zero);
+ for (int i = 0; i < 50; i++)
+ {
+ var offset = new TimeSpan((long)(i * scale));
+ DbContext.Add(new Commit
+ {
+ ClientId = Guid.NewGuid(),
+ HybridDateTime = new HybridDateTime(baseDateTime.Add(offset), 0)
+ });
+ }
+
+ await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken);
+ var commits = await DbContext.Commits
+ .Where(c => c.HybridDateTime.DateTime > baseDateTime.Add(new TimeSpan((long)(25 * scale))))
+ .OrderBy(c => c.HybridDateTime.DateTime)
+ .ToArrayAsyncEF(TestContext.Current.CancellationToken);
+ commits.Should().HaveCount(24);
+ }
+}
diff --git a/src/SIL.Harmony.Tests/DefinitionTests.cs b/src/SIL.Harmony.Tests/DefinitionTests.cs
index 49e664c..40ceac0 100644
--- a/src/SIL.Harmony.Tests/DefinitionTests.cs
+++ b/src/SIL.Harmony.Tests/DefinitionTests.cs
@@ -1,107 +1,107 @@
-using SIL.Harmony.Changes;
-using SIL.Harmony.Sample.Models;
-using Microsoft.EntityFrameworkCore;
-
-namespace SIL.Harmony.Tests;
-
-public class DefinitionTests : DataModelTestBase
-{
- [Fact]
- public async Task CanAddADefinitionToAWord()
- {
- var wordId = Guid.NewGuid();
- await WriteNextChange(SetWord(wordId, "hello"));
- await WriteNextChange(NewDefinition(wordId, "a greeting", "verb"));
- var snapshot = await DataModel.GetProjectSnapshot();
- var definitionSnapshot = snapshot.Snapshots.Values.Single(s => s.IsType());
- var definition = await DataModel.GetBySnapshotId(definitionSnapshot.Id);
- definition.Text.Should().Be("a greeting");
- definition.WordId.Should().Be(wordId);
- }
-
- [Fact]
- public async Task DeletingAWordDeletesTheDefinition()
- {
- var wordId = Guid.NewGuid();
- await WriteNextChange(SetWord(wordId, "hello"));
- await WriteNextChange(NewDefinition(wordId, "a greeting", "verb"));
- await WriteNextChange(new DeleteChange(wordId));
- var snapshot = await DataModel.GetProjectSnapshot();
- snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty();
- }
-
- [Fact]
- public async Task AddingADefinitionToADeletedWordDeletesIt()
- {
- var wordId = Guid.NewGuid();
- await WriteNextChange(SetWord(wordId, "hello"));
- await WriteNextChange(new DeleteChange(wordId));
- await WriteNextChange(NewDefinition(wordId, "a greeting", "verb"));
- var snapshot = await DataModel.GetProjectSnapshot();
- snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty();
- }
-
-
- [Fact]
- public async Task CanGetInOrder()
- {
- var wordId = Guid.NewGuid();
- await WriteNextChange(SetWord(wordId, "hello"));
- await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2));
- await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1));
-
- var definitions = await DataModel.QueryLatest().ToArrayAsync();
- definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder(
- "noun",
- "verb"
- );
- }
-
- [Fact]
- public async Task CanChangeOrderBetweenExistingDefinitions()
- {
- var wordId = Guid.NewGuid();
- var definitionAId = Guid.NewGuid();
- var definitionBId = Guid.NewGuid();
- var definitionCId = Guid.NewGuid();
- await WriteNextChange(SetWord(wordId, "hello"));
- await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionAId));
- await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2, definitionBId));
- await WriteNextChange(NewDefinition(wordId, "used as a greeting", "exclamation", 3, definitionCId));
-
- var definitions = await DataModel.QueryLatest().ToArrayAsync();
- definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder(
- "noun",
- "verb",
- "exclamation"
- );
-
- //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.QueryLatest().ToArrayAsync();
- definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder(
- "noun",
- "exclamation",
- "verb"
- );
- }
-
- [Fact]
- public async Task ConsistentlySortsItems()
- {
- var wordId = Guid.NewGuid();
- //these ids are hardcoded to ensure the order is consistent
- var definitionAId = new Guid("a40fb7c8-f3b1-441b-b5fb-0261ae32bff1");
- var definitionBId = new Guid("0197b80d-9691-4896-8c9b-277a3455a7f6");
- await WriteNextChange(SetWord(wordId, "hello"));
- await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 1, definitionAId));
- await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionBId));
-
- var definitions = await DataModel.QueryLatest().ToArrayAsync();
- definitions.Select(d => d.Id).Should().ContainInConsecutiveOrder(
- definitionBId,
- definitionAId
- );
- }
+using SIL.Harmony.Changes;
+using SIL.Harmony.Sample.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace SIL.Harmony.Tests;
+
+public class DefinitionTests : DataModelTestBase
+{
+ [Fact]
+ public async Task CanAddADefinitionToAWord()
+ {
+ var wordId = Guid.NewGuid();
+ await WriteNextChange(SetWord(wordId, "hello"));
+ await WriteNextChange(NewDefinition(wordId, "a greeting", "verb"));
+ var snapshot = await DataModel.GetProjectSnapshot();
+ var definitionSnapshot = snapshot.Snapshots.Values.Single(s => s.IsType());
+ var definition = await DataModel.GetBySnapshotId(definitionSnapshot.Id);
+ definition.Text.Should().Be("a greeting");
+ definition.WordId.Should().Be(wordId);
+ }
+
+ [Fact]
+ public async Task DeletingAWordDeletesTheDefinition()
+ {
+ var wordId = Guid.NewGuid();
+ await WriteNextChange(SetWord(wordId, "hello"));
+ await WriteNextChange(NewDefinition(wordId, "a greeting", "verb"));
+ await WriteNextChange(new DeleteChange(wordId));
+ var snapshot = await DataModel.GetProjectSnapshot();
+ snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task AddingADefinitionToADeletedWordDeletesIt()
+ {
+ var wordId = Guid.NewGuid();
+ await WriteNextChange(SetWord(wordId, "hello"));
+ await WriteNextChange(new DeleteChange(wordId));
+ await WriteNextChange(NewDefinition(wordId, "a greeting", "verb"));
+ var snapshot = await DataModel.GetProjectSnapshot();
+ snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty();
+ }
+
+
+ [Fact]
+ public async Task CanGetInOrder()
+ {
+ var wordId = Guid.NewGuid();
+ await WriteNextChange(SetWord(wordId, "hello"));
+ await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2));
+ await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1));
+
+ var definitions = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken);
+ definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder(
+ "noun",
+ "verb"
+ );
+ }
+
+ [Fact]
+ public async Task CanChangeOrderBetweenExistingDefinitions()
+ {
+ var wordId = Guid.NewGuid();
+ var definitionAId = Guid.NewGuid();
+ var definitionBId = Guid.NewGuid();
+ var definitionCId = Guid.NewGuid();
+ await WriteNextChange(SetWord(wordId, "hello"));
+ await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionAId));
+ await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2, definitionBId));
+ await WriteNextChange(NewDefinition(wordId, "used as a greeting", "exclamation", 3, definitionCId));
+
+ var definitions = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken);
+ definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder(
+ "noun",
+ "verb",
+ "exclamation"
+ );
+
+ //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.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken);
+ definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder(
+ "noun",
+ "exclamation",
+ "verb"
+ );
+ }
+
+ [Fact]
+ public async Task ConsistentlySortsItems()
+ {
+ var wordId = Guid.NewGuid();
+ //these ids are hardcoded to ensure the order is consistent
+ var definitionAId = new Guid("a40fb7c8-f3b1-441b-b5fb-0261ae32bff1");
+ var definitionBId = new Guid("0197b80d-9691-4896-8c9b-277a3455a7f6");
+ await WriteNextChange(SetWord(wordId, "hello"));
+ await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 1, definitionAId));
+ await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionBId));
+
+ var definitions = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken);
+ definitions.Select(d => d.Id).Should().ContainInConsecutiveOrder(
+ definitionBId,
+ definitionAId
+ );
+ }
}
\ No newline at end of file
diff --git a/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs b/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs
index 409f8bd..0397054 100644
--- a/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs
+++ b/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs
@@ -25,7 +25,7 @@ await WriteNextChange(
word.DeletedAt.Should().BeNull();
word.Text.Should().Be("Undeleted");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.DeletedAt.Should().BeNull();
entityWord.Text.Should().Be("Undeleted");
@@ -48,7 +48,7 @@ await WriteNextChange(new NewWordChange(wordId, "Undeleted"), add: false),
word.DeletedAt.Should().BeNull();
word.Text.Should().Be("Undeleted");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.DeletedAt.Should().BeNull();
entityWord.Text.Should().Be("Undeleted");
@@ -72,7 +72,7 @@ await WriteNextChange([
word.DeletedAt.Should().BeNull();
word.Text.Should().Be("Undeleted");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.DeletedAt.Should().BeNull();
entityWord.Text.Should().Be("Undeleted");
@@ -96,7 +96,7 @@ await WriteNextChange(new NewWordChange(wordId, "Undeleted"), add: false),
word.DeletedAt.Should().BeNull();
word.Text.Should().Be("Undeleted");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.DeletedAt.Should().BeNull();
entityWord.Text.Should().Be("Undeleted");
@@ -119,7 +119,7 @@ await WriteNextChange(
word.DeletedAt.Should().BeNull();
word.Text.Should().Be("Undeleted");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.DeletedAt.Should().BeNull();
entityWord.Text.Should().Be("Undeleted");
@@ -141,7 +141,7 @@ await WriteNextChange(new NewWordChange(wordId, "Undeleted"), add: false),
word.DeletedAt.Should().BeNull();
word.Text.Should().Be("Undeleted");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.DeletedAt.Should().BeNull();
entityWord.Text.Should().Be("Undeleted");
@@ -163,7 +163,7 @@ await WriteNextChange(
word.DeletedAt.Should().NotBeNull();
word.Text.Should().Be("original");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().BeNull();
}
@@ -182,7 +182,7 @@ await WriteNextChange(new DeleteChange(wordId), add: false),
word.DeletedAt.Should().NotBeNull();
word.Text.Should().Be("original");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().BeNull();
}
@@ -192,7 +192,7 @@ public async Task NewEntityOnExistingEntityIsNoOp()
var wordId = Guid.NewGuid();
await WriteNextChange(new NewWordChange(wordId, "original"));
- var snapshotsBefore = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync();
+ var snapshotsBefore = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync(TestContext.Current.CancellationToken);
await WriteNextChange(
[
@@ -204,12 +204,12 @@ await WriteNextChange(
word.DeletedAt.Should().BeNull();
word.Text.Should().Be("original");
- var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync();
+ var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken);
entityWord.Should().NotBeNull();
entityWord.DeletedAt.Should().BeNull();
entityWord.Text.Should().Be("original");
- var snapshotsAfter = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync();
+ var snapshotsAfter = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync(TestContext.Current.CancellationToken);
snapshotsAfter.Select(s => s.Id).Should().BeEquivalentTo(snapshotsBefore.Select(s => s.Id));
}
}
diff --git a/src/SIL.Harmony.Tests/ModelSnapshotTests.cs b/src/SIL.Harmony.Tests/ModelSnapshotTests.cs
index 639f40e..d4455f8 100644
--- a/src/SIL.Harmony.Tests/ModelSnapshotTests.cs
+++ b/src/SIL.Harmony.Tests/ModelSnapshotTests.cs
@@ -1,142 +1,142 @@
-using SIL.Harmony.Sample.Models;
-using Microsoft.EntityFrameworkCore;
-
-namespace SIL.Harmony.Tests;
-
-public class ModelSnapshotTests : DataModelTestBase
-{
- [Fact]
- public void CanGetEmptyModelSnapshot()
- {
- DataModel.GetProjectSnapshot().Should().NotBeNull();
- }
-
- [Fact]
- public async Task CanGetModelSnapshot()
- {
- await WriteNextChange(SetWord(Guid.NewGuid(), "entity1"));
- await WriteNextChange(SetWord(Guid.NewGuid(), "entity2"));
- var snapshot = await DataModel.GetProjectSnapshot();
- snapshot.Snapshots.Should().HaveCount(2);
- }
-
- [Fact]
- public async Task ModelSnapshotShowsMultipleChanges()
- {
- var entityId = Guid.NewGuid();
- await WriteNextChange(SetWord(entityId, "first"));
- var secondChange = await WriteNextChange(SetWord(entityId, "second"));
- var snapshot = await DataModel.GetProjectSnapshot();
- var simpleSnapshot = snapshot.Snapshots.Values.First();
- var entry = await DataModel.GetBySnapshotId(simpleSnapshot.Id);
- entry.Text.Should().Be("second");
- snapshot.LastChange.Should().Be(secondChange.DateTime);
- }
-
- [Fact]
- public async Task CanGetWordForASpecificCommit()
- {
- var entityId = Guid.NewGuid();
- var firstCommit = await WriteNextChange(SetWord(entityId, "first"));
- var secondCommit = await WriteNextChange(SetWord(entityId, "second"));
- var thirdCommit = await WriteNextChange(SetWord(entityId, "third"));
- await ClearNonRootSnapshots();
- var firstWord = await DataModel.GetAtCommit(firstCommit.Id, entityId);
- firstWord.Should().NotBeNull();
- firstWord.Text.Should().Be("first");
-
- var secondWord = await DataModel.GetAtCommit(secondCommit.Id, entityId);
- secondWord.Should().NotBeNull();
- secondWord.Text.Should().Be("second");
-
- var thirdWord = await DataModel.GetAtCommit(thirdCommit.Id, entityId);
- thirdWord.Should().NotBeNull();
- thirdWord.Text.Should().Be("third");
- }
-
- [Fact]
- public async Task CanGetWordForASpecificTime()
- {
- var entityId = Guid.NewGuid();
- var firstCommit = await WriteNextChange(SetWord(entityId, "first"));
- var secondCommit = await WriteNextChange(SetWord(entityId, "second"));
- var thirdCommit = await WriteNextChange(SetWord(entityId, "third"));
- //ensures that SnapshotWorker.ApplyCommitsToSnapshots will be called when getting the snapshots
- await ClearNonRootSnapshots();
- var firstWord = await DataModel.GetAtTime(firstCommit.DateTime.AddMinutes(5), entityId);
- firstWord.Should().NotBeNull();
- firstWord.Text.Should().Be("first");
-
- var secondWord = await DataModel.GetAtTime(secondCommit.DateTime.AddMinutes(5), entityId);
- secondWord.Should().NotBeNull();
- secondWord.Text.Should().Be("second");
-
- //just before the 3rd commit should still be second
- secondWord = await DataModel.GetAtTime(thirdCommit.DateTime.Subtract(TimeSpan.FromSeconds(5)), entityId);
- secondWord.Should().NotBeNull();
- secondWord.Text.Should().Be("second");
-
- var thirdWord = await DataModel.GetAtTime(thirdCommit.DateTime.AddMinutes(5), entityId);
- thirdWord.Should().NotBeNull();
- thirdWord.Text.Should().Be("third");
- }
-
- private Task ClearNonRootSnapshots()
- {
- return DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync();
- }
-
- [Theory]
- [InlineData(10)]
- [InlineData(100)]
- // [InlineData(1_000)]
- public async Task CanGetSnapshotFromEarlier(int changeCount)
- {
- var entityId = Guid.NewGuid();
- await WriteNextChange(SetWord(entityId, "first"));
- var changes = new List(changeCount);
- var addNew = new List(changeCount);
- for (var i = 0; i < changeCount; i++)
- {
- // todo: these commits all have an odd index, so no intermediate snapshots will be persisted i.e. the snapshot count checking is somewhat deceptive
- changes.Add(await WriteNextChange(SetWord(entityId, $"change {i}"), false).AsTask());
- addNew.Add(await WriteNextChange(SetWord(Guid.NewGuid(), $"add {i}"), false).AsTask());
- }
-
- //adding all via sync means there's sparse snapshots
- await AddCommitsViaSync(changes.Concat(addNew));
- //there will only be a snapshot for every other commit, but there's change count * 2 commits, plus a first and last change
- DbContext.Snapshots.Should().HaveCount(2 + changeCount);
-
- for (int i = 0; i < changeCount; i++)
- {
- var snapshots = await DataModel.GetSnapshotsAtCommit(changes[i]);
- var entry = snapshots[entityId].Entity.Is();
- entry.Text.Should().Be($"change {i}");
- snapshots.Values.Should().HaveCount(1 + i);
- }
- }
-
- [Fact]
- public async Task WorstCaseSnapshotReApply()
- {
- int changeCount = 1_000;
- var entityId = Guid.NewGuid();
- await WriteNextChange(SetWord(entityId, "first"));
- //adding all in one AddRange means there's sparse snapshots
- await AddCommitsViaSync(Enumerable.Range(0, changeCount)
- .Select(i => WriteNextChange(SetWord(entityId, $"change {i}"), false).Result));
-
- var latestSnapshot = await DataModel.GetLatestSnapshotByObjectId(entityId);
- //delete snapshots so when we get at then we need to re-apply
- await DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync();
-
- var computedModelSnapshots = await DataModel.GetSnapshotsAtCommit(latestSnapshot.Commit);
-
- var entitySnapshot = computedModelSnapshots.Should().ContainSingle().Subject.Value;
- entitySnapshot.Should().BeEquivalentTo(latestSnapshot, options => options.Excluding(snapshot => snapshot.Id).Excluding(snapshot => snapshot.Commit).Excluding(s => s.Entity.DbObject));
- var latestSnapshotEntry = latestSnapshot.Entity.Is();
- var entitySnapshotEntry = entitySnapshot.Entity.Is();
- entitySnapshotEntry.Text.Should().Be(latestSnapshotEntry.Text);
- }
-}
+using SIL.Harmony.Sample.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace SIL.Harmony.Tests;
+
+public class ModelSnapshotTests : DataModelTestBase
+{
+ [Fact]
+ public void CanGetEmptyModelSnapshot()
+ {
+ DataModel.GetProjectSnapshot().Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task CanGetModelSnapshot()
+ {
+ await WriteNextChange(SetWord(Guid.NewGuid(), "entity1"));
+ await WriteNextChange(SetWord(Guid.NewGuid(), "entity2"));
+ var snapshot = await DataModel.GetProjectSnapshot();
+ snapshot.Snapshots.Should().HaveCount(2);
+ }
+
+ [Fact]
+ public async Task ModelSnapshotShowsMultipleChanges()
+ {
+ var entityId = Guid.NewGuid();
+ await WriteNextChange(SetWord(entityId, "first"));
+ var secondChange = await WriteNextChange(SetWord(entityId, "second"));
+ var snapshot = await DataModel.GetProjectSnapshot();
+ var simpleSnapshot = snapshot.Snapshots.Values.First();
+ var entry = await DataModel.GetBySnapshotId(simpleSnapshot.Id);
+ entry.Text.Should().Be("second");
+ snapshot.LastChange.Should().Be(secondChange.DateTime);
+ }
+
+ [Fact]
+ public async Task CanGetWordForASpecificCommit()
+ {
+ var entityId = Guid.NewGuid();
+ var firstCommit = await WriteNextChange(SetWord(entityId, "first"));
+ var secondCommit = await WriteNextChange(SetWord(entityId, "second"));
+ var thirdCommit = await WriteNextChange(SetWord(entityId, "third"));
+ await ClearNonRootSnapshots();
+ var firstWord = await DataModel.GetAtCommit(firstCommit.Id, entityId);
+ firstWord.Should().NotBeNull();
+ firstWord.Text.Should().Be("first");
+
+ var secondWord = await DataModel.GetAtCommit(secondCommit.Id, entityId);
+ secondWord.Should().NotBeNull();
+ secondWord.Text.Should().Be("second");
+
+ var thirdWord = await DataModel.GetAtCommit(thirdCommit.Id, entityId);
+ thirdWord.Should().NotBeNull();
+ thirdWord.Text.Should().Be("third");
+ }
+
+ [Fact]
+ public async Task CanGetWordForASpecificTime()
+ {
+ var entityId = Guid.NewGuid();
+ var firstCommit = await WriteNextChange(SetWord(entityId, "first"));
+ var secondCommit = await WriteNextChange(SetWord(entityId, "second"));
+ var thirdCommit = await WriteNextChange(SetWord(entityId, "third"));
+ //ensures that SnapshotWorker.ApplyCommitsToSnapshots will be called when getting the snapshots
+ await ClearNonRootSnapshots();
+ var firstWord = await DataModel.GetAtTime(firstCommit.DateTime.AddMinutes(5), entityId);
+ firstWord.Should().NotBeNull();
+ firstWord.Text.Should().Be("first");
+
+ var secondWord = await DataModel.GetAtTime(secondCommit.DateTime.AddMinutes(5), entityId);
+ secondWord.Should().NotBeNull();
+ secondWord.Text.Should().Be("second");
+
+ //just before the 3rd commit should still be second
+ secondWord = await DataModel.GetAtTime(thirdCommit.DateTime.Subtract(TimeSpan.FromSeconds(5)), entityId);
+ secondWord.Should().NotBeNull();
+ secondWord.Text.Should().Be("second");
+
+ var thirdWord = await DataModel.GetAtTime(thirdCommit.DateTime.AddMinutes(5), entityId);
+ thirdWord.Should().NotBeNull();
+ thirdWord.Text.Should().Be("third");
+ }
+
+ private Task ClearNonRootSnapshots()
+ {
+ return DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync();
+ }
+
+ [Theory]
+ [InlineData(10)]
+ [InlineData(100)]
+ // [InlineData(1_000)]
+ public async Task CanGetSnapshotFromEarlier(int changeCount)
+ {
+ var entityId = Guid.NewGuid();
+ await WriteNextChange(SetWord(entityId, "first"));
+ var changes = new List(changeCount);
+ var addNew = new List(changeCount);
+ for (var i = 0; i < changeCount; i++)
+ {
+ // todo: these commits all have an odd index, so no intermediate snapshots will be persisted i.e. the snapshot count checking is somewhat deceptive
+ changes.Add(await WriteNextChange(SetWord(entityId, $"change {i}"), false).AsTask());
+ addNew.Add(await WriteNextChange(SetWord(Guid.NewGuid(), $"add {i}"), false).AsTask());
+ }
+
+ //adding all via sync means there's sparse snapshots
+ await AddCommitsViaSync(changes.Concat(addNew));
+ //there will only be a snapshot for every other commit, but there's change count * 2 commits, plus a first and last change
+ DbContext.Snapshots.Should().HaveCount(2 + changeCount);
+
+ for (int i = 0; i < changeCount; i++)
+ {
+ var snapshots = await DataModel.GetSnapshotsAtCommit(changes[i]);
+ var entry = snapshots[entityId].Entity.Is();
+ entry.Text.Should().Be($"change {i}");
+ snapshots.Values.Should().HaveCount(1 + i);
+ }
+ }
+
+ [Fact]
+ public async Task WorstCaseSnapshotReApply()
+ {
+ int changeCount = 1_000;
+ var entityId = Guid.NewGuid();
+ await WriteNextChange(SetWord(entityId, "first"));
+ //adding all in one AddRange means there's sparse snapshots
+ await AddCommitsViaSync(Enumerable.Range(0, changeCount)
+ .Select(i => WriteNextChange(SetWord(entityId, $"change {i}"), false).Result));
+
+ var latestSnapshot = await DataModel.GetLatestSnapshotByObjectId(entityId);
+ //delete snapshots so when we get at then we need to re-apply
+ await DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync(TestContext.Current.CancellationToken);
+
+ var computedModelSnapshots = await DataModel.GetSnapshotsAtCommit(latestSnapshot.Commit);
+
+ var entitySnapshot = computedModelSnapshots.Should().ContainSingle().Subject.Value;
+ entitySnapshot.Should().BeEquivalentTo(latestSnapshot, options => options.Excluding(snapshot => snapshot.Id).Excluding(snapshot => snapshot.Commit).Excluding(s => s.Entity.DbObject));
+ var latestSnapshotEntry = latestSnapshot.Entity.Is();
+ var entitySnapshotEntry = entitySnapshot.Entity.Is();
+ entitySnapshotEntry.Text.Should().Be(latestSnapshotEntry.Text);
+ }
+}
diff --git a/src/SIL.Harmony.Tests/MultiThreadingTests.cs b/src/SIL.Harmony.Tests/MultiThreadingTests.cs
index 6d68314..6efee0f 100644
--- a/src/SIL.Harmony.Tests/MultiThreadingTests.cs
+++ b/src/SIL.Harmony.Tests/MultiThreadingTests.cs
@@ -1,5 +1,4 @@
using Microsoft.Data.Sqlite;
-using Xunit.Abstractions;
namespace SIL.Harmony.Tests;
@@ -17,7 +16,7 @@ public class MultiThreadingTests(ITestOutputHelper output)
{
var random = new Random();
var fixture = new DataModelTestBase(new SqliteConnection(_connectionString));
- fixture.InitializeAsync().Wait();
+ fixture.InitializeAsync().GetAwaiter().GetResult();
var id = Guid.NewGuid();
for (var i = 0; i < 100; i++)
{
diff --git a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs
index 35e8815..0de54a8 100644
--- a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs
+++ b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs
@@ -76,7 +76,7 @@ public async Task CanPersistExtraData()
{
var entityId = Guid.NewGuid();
var commit = await _dataModelTestBase.WriteNextChange(new CreateExtraDataModelChange(entityId));
- var extraDataModel = _dataModelTestBase.DataModel.QueryLatest().ToBlockingEnumerable().Should().ContainSingle().Subject;
+ var extraDataModel = _dataModelTestBase.DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Should().ContainSingle().Subject;
extraDataModel.Id.Should().Be(entityId);
extraDataModel.CommitId.Should().Be(commit.Id);
extraDataModel.DateTime.Should().Be(commit.HybridDateTime.DateTime);
diff --git a/src/SIL.Harmony.Tests/RepositoryTests.cs b/src/SIL.Harmony.Tests/RepositoryTests.cs
index b3e23ad..5fac8a6 100644
--- a/src/SIL.Harmony.Tests/RepositoryTests.cs
+++ b/src/SIL.Harmony.Tests/RepositoryTests.cs
@@ -25,14 +25,14 @@ public RepositoryTests()
_crdtDbContext = _services.GetRequiredService();
}
- public async Task InitializeAsync()
+ public async ValueTask InitializeAsync()
{
// Open the connection manually, otherwise it will be closed after each command, wiping out the in memory sqlite db
await _crdtDbContext.Database.OpenConnectionAsync();
await _crdtDbContext.Database.EnsureCreatedAsync();
}
- public async Task DisposeAsync()
+ public async ValueTask DisposeAsync()
{
await _repository.DisposeAsync();
await _services.DisposeAsync();
@@ -115,7 +115,7 @@ await _repository.AddCommits([
Commit(Guid.NewGuid(), commit1Time),
Commit(Guid.NewGuid(), commit2Time),
]);
- var commits = await _repository.CurrentCommits().ToArrayAsync();
+ var commits = await _repository.CurrentCommits().ToArrayAsync(TestContext.Current.CancellationToken);
commits.Select(c => c.HybridDateTime).Should().ContainInConsecutiveOrder(commit1Time, commit2Time);
}
@@ -128,7 +128,7 @@ await _repository.AddCommits([
Commit(Guid.NewGuid(), commit1Time),
Commit(Guid.NewGuid(), commit2Time),
]);
- var commits = await _repository.CurrentCommits().ToArrayAsync();
+ var commits = await _repository.CurrentCommits().ToArrayAsync(TestContext.Current.CancellationToken);
commits.Select(c => c.HybridDateTime).Should().ContainInConsecutiveOrder(commit1Time, commit2Time);
}
@@ -141,7 +141,7 @@ await _repository.AddCommits([
Commit(ids[0], commitTime),
Commit(ids[1], commitTime),
]);
- var commits = await _repository.CurrentCommits().ToArrayAsync();
+ var commits = await _repository.CurrentCommits().ToArrayAsync(TestContext.Current.CancellationToken);
commits.Select(c => c.Id).Should().ContainInConsecutiveOrder(ids);
}
@@ -149,7 +149,7 @@ await _repository.AddCommits([
public async Task CurrentSnapshots_Works()
{
await _repository.AddSnapshots([Snapshot(Guid.NewGuid(), Guid.NewGuid(), Time(1, 0))]);
- var snapshots = await _repository.CurrentSnapshots().ToArrayAsync();
+ var snapshots = await _repository.CurrentSnapshots().ToArrayAsync(TestContext.Current.CancellationToken);
snapshots.Should().ContainSingle();
}
@@ -162,7 +162,7 @@ await _repository.AddSnapshots([
Snapshot(entityId, Guid.NewGuid(), Time(1, 0)),
Snapshot(entityId, Guid.NewGuid(), expectedTime),
]);
- var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync();
+ var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken);
snapshots.Should().ContainSingle().Which.Commit.HybridDateTime.Should().BeEquivalentTo(expectedTime);
}
@@ -174,7 +174,7 @@ await _repository.AddSnapshots([
Snapshot(entityId, Guid.NewGuid(), Time(1, 0)),
Snapshot(entityId, Guid.NewGuid(), Time(1, 1)),
]);
- var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync();
+ var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken);
snapshots.Should().ContainSingle().Which.Commit.HybridDateTime.Counter.Should().Be(1);
}
@@ -188,7 +188,7 @@ await _repository.AddSnapshots([
Snapshot(entityId, ids[0], time),
Snapshot(entityId, ids[1], time),
]);
- var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync();
+ var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken);
snapshots.Should().ContainSingle().Which.Commit.Id.Should().Be(ids[1]);
}
@@ -207,12 +207,12 @@ await _repository.AddSnapshots([
snapshot2,
]);
- var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync();
+ var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken);
var commit = snapshots.Should().ContainSingle().Subject.Commit;
commit.Id.Should().Be(commitIds[2]);
snapshots = await _repository.GetScopedRepository(snapshot2.Commit).CurrentSnapshots().Include(s => s.Commit)
- .ToArrayAsync();
+ .ToArrayAsync(TestContext.Current.CancellationToken);
commit = snapshots.Should().ContainSingle().Subject.Commit;
commit.Id.Should().Be(commitIds[1], $"commit order: [{string.Join(", ", commitIds)}]");
}
@@ -232,11 +232,11 @@ await _repository.AddSnapshots([
snapshot2,
]);
- var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync();
+ var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken);
var commit = snapshots.Should().ContainSingle().Subject.Commit;
commit.Id.Should().Be(commitIds[2]);
- snapshots = await _repository.GetScopedRepository(snapshot2.Commit).CurrentSnapshots().Include(s => s.Commit).ToArrayAsync();
+ snapshots = await _repository.GetScopedRepository(snapshot2.Commit).CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken);
commit = snapshots.Should().ContainSingle().Subject.Commit;
commit.Id.Should().Be(commitIds[1], $"commit order: [{string.Join(", ", commitIds)}]");
}
diff --git a/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs b/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
index 59e9b84..214761d 100644
--- a/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
+++ b/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
@@ -128,7 +128,7 @@ public async Task CanDownloadFileFromRemote()
localResource.Id.Should().Be(resourceId);
- var actualFileContents = await File.ReadAllTextAsync(localResource.LocalPath);
+ var actualFileContents = await File.ReadAllTextAsync(localResource.LocalPath, TestContext.Current.CancellationToken);
actualFileContents.Should().Be(fileContents);
var pendingDownloads = await _resourceService.ListResourcesPendingDownload();
pendingDownloads.Should().BeEmpty();
diff --git a/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs b/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs
index e7fc702..a52e101 100644
--- a/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs
+++ b/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs
@@ -1,41 +1,41 @@
-using System.Runtime.CompilerServices;
-using Microsoft.Extensions.DependencyInjection;
-using SIL.Harmony.Sample.Changes;
-using SIL.Harmony.Sample.Models;
-
-namespace SIL.Harmony.Tests.ResourceTests;
-
-public class WordResourceTests: DataModelTestBase
-{
- private RemoteServiceMock _remoteServiceMock = new();
- private ResourceService _resourceService => _services.GetRequiredService();
- private readonly Guid _entity1Id = Guid.NewGuid();
-
- private string CreateFile(string contents, [CallerMemberName] string fileName = "")
- {
- var filePath = Path.GetFullPath(fileName + ".txt");
- File.WriteAllText(filePath, contents);
- return filePath;
- }
-
- [Fact]
- public async Task CanReferenceAResourceFromAWord()
- {
- await WriteNextChange(SetWord(_entity1Id, "test-value"));
- var imageFile = CreateFile("not image data");
- //set commit date for add local resource
- MockTimeProvider.SetNextDateTime(NextDate());
- var resource = await _resourceService.AddLocalResource(imageFile, Guid.NewGuid(), resourceService: _remoteServiceMock);
- await WriteNextChange(new AddWordImageChange(_entity1Id, resource.Id));
-
- var word = await DataModel.GetLatest(_entity1Id);
- word.Should().NotBeNull();
- word!.ImageResourceId.Should().Be(resource.Id);
-
-
- var localResource = await _resourceService.GetLocalResource(word.ImageResourceId!.Value);
- localResource.Should().NotBeNull();
- localResource!.LocalPath.Should().Be(imageFile);
- (await File.ReadAllTextAsync(localResource.LocalPath)).Should().Be("not image data");
- }
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
+using SIL.Harmony.Sample.Changes;
+using SIL.Harmony.Sample.Models;
+
+namespace SIL.Harmony.Tests.ResourceTests;
+
+public class WordResourceTests: DataModelTestBase
+{
+ private RemoteServiceMock _remoteServiceMock = new();
+ private ResourceService _resourceService => _services.GetRequiredService();
+ private readonly Guid _entity1Id = Guid.NewGuid();
+
+ private string CreateFile(string contents, [CallerMemberName] string fileName = "")
+ {
+ var filePath = Path.GetFullPath(fileName + ".txt");
+ File.WriteAllText(filePath, contents);
+ return filePath;
+ }
+
+ [Fact]
+ public async Task CanReferenceAResourceFromAWord()
+ {
+ await WriteNextChange(SetWord(_entity1Id, "test-value"));
+ var imageFile = CreateFile("not image data");
+ //set commit date for add local resource
+ MockTimeProvider.SetNextDateTime(NextDate());
+ var resource = await _resourceService.AddLocalResource(imageFile, Guid.NewGuid(), resourceService: _remoteServiceMock);
+ await WriteNextChange(new AddWordImageChange(_entity1Id, resource.Id));
+
+ var word = await DataModel.GetLatest(_entity1Id);
+ word.Should().NotBeNull();
+ word!.ImageResourceId.Should().Be(resource.Id);
+
+
+ var localResource = await _resourceService.GetLocalResource(word.ImageResourceId!.Value);
+ localResource.Should().NotBeNull();
+ localResource!.LocalPath.Should().Be(imageFile);
+ (await File.ReadAllTextAsync(localResource.LocalPath, TestContext.Current.CancellationToken)).Should().Be("not image data");
+ }
}
\ No newline at end of file
diff --git a/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj b/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj
index fdfa563..f542263 100644
--- a/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj
+++ b/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj
@@ -9,16 +9,16 @@
+
all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -28,6 +28,8 @@
all
+
+
diff --git a/src/SIL.Harmony.Tests/SnapshotTests.cs b/src/SIL.Harmony.Tests/SnapshotTests.cs
index 1a4ff0e..3a9028b 100644
--- a/src/SIL.Harmony.Tests/SnapshotTests.cs
+++ b/src/SIL.Harmony.Tests/SnapshotTests.cs
@@ -31,7 +31,7 @@ public async Task MultipleChangesPreservesRootSnapshot()
await AddCommitsViaSync(commits);
- var snapshots = await DbContext.Snapshots.ToArrayAsync();
+ var snapshots = await DbContext.Snapshots.ToArrayAsync(TestContext.Current.CancellationToken);
snapshots.Should().HaveCountGreaterThan(1);
snapshots.Should().ContainSingle(s => s.IsRoot);
}
@@ -52,7 +52,7 @@ public async Task MultipleChangesPreservesSomeIntermediateSnapshots()
await AddCommitsViaSync(commits);
var latestSnapshot = await DataModel.GetLatestSnapshotByObjectId(entityId);
- var snapshots = await DbContext.Snapshots.ToArrayAsync();
+ var snapshots = await DbContext.Snapshots.ToArrayAsync(TestContext.Current.CancellationToken);
snapshots.Should().HaveCountGreaterThan(2);
snapshots.Should().ContainSingle(s => s.Id == latestSnapshot.Id);
snapshots.Should().ContainSingle(s => s.IsRoot);
@@ -119,7 +119,7 @@ await WriteNextChange(
]);
var word = await DataModel.QueryLatest(q => q.Include(w => w.Tags)
- .Where(w => w.Id == wordId)).FirstOrDefaultAsync();
+ .Where(w => w.Id == wordId)).FirstOrDefaultAsync(TestContext.Current.CancellationToken);
word.Should().NotBeNull();
word.Tags.Should().BeEquivalentTo([new Tag { Id = tagId, Text = "tag-1" }]);
}
@@ -138,7 +138,7 @@ await WriteNextChange(
await WriteChangeBefore(tagCreation, TagWord(wordId, tagId));
var word = await DataModel.QueryLatest(q => q.Include(w => w.Tags)
- .Where(w => w.Id == wordId)).FirstOrDefaultAsync();
+ .Where(w => w.Id == wordId)).FirstOrDefaultAsync(TestContext.Current.CancellationToken);
word.Should().NotBeNull();
word.Tags.Should().BeEquivalentTo([new Tag { Id = tagId, Text = "tag-1" }]);
}
@@ -151,13 +151,13 @@ public async Task RegenerateSnapshots_WillArriveAtTheSameState()
await WriteNextChange(SetWord(entityId, "test1"));
await WriteNextChange(SetWord(entityId, "test2"));
await WriteNextChange(SetWord(entityId, "test3"));
- var beforeSnapshotIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync();
- var beforeRegenerate = await DataModel.QueryLatest().ToArrayAsync();
+ var beforeSnapshotIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync(TestContext.Current.CancellationToken);
+ var beforeRegenerate = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken);
await DataModel.RegenerateSnapshots();
- var afterRegenerate = await DataModel.QueryLatest().ToArrayAsync();
- var afterSnapshotsIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync();
+ var afterRegenerate = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken);
+ var afterSnapshotsIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync(TestContext.Current.CancellationToken);
afterRegenerate.Should().BeEquivalentTo(beforeRegenerate);
//we probably won't have the same number of snapshots, which is ok. but none of the ids should be the same
diff --git a/src/SIL.Harmony.Tests/SyncTests.cs b/src/SIL.Harmony.Tests/SyncTests.cs
index bc9985a..15cf8b5 100644
--- a/src/SIL.Harmony.Tests/SyncTests.cs
+++ b/src/SIL.Harmony.Tests/SyncTests.cs
@@ -8,13 +8,13 @@ public class SyncTests : IAsyncLifetime
private readonly DataModelTestBase _client1 = new();
private readonly DataModelTestBase _client2 = new();
- public async Task InitializeAsync()
+ public async ValueTask InitializeAsync()
{
await _client1.InitializeAsync();
await _client2.InitializeAsync();
}
- public async Task DisposeAsync()
+ public async ValueTask DisposeAsync()
{
await _client1.DisposeAsync();
await _client2.DisposeAsync();
@@ -116,7 +116,7 @@ public async Task CanSync_AddDependentWithMultipleChanges()
await _client2.DataModel.SyncWith(_client1.DataModel);
- _client2.DataModel.QueryLatest().ToBlockingEnumerable().Should()
- .BeEquivalentTo(_client1.DataModel.QueryLatest().ToBlockingEnumerable());
+ _client2.DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Should()
+ .BeEquivalentTo(_client1.DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken));
}
}
\ No newline at end of file
diff --git a/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs b/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs
index 9694bb5..38ebe34 100644
--- a/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs
+++ b/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs
@@ -1,34 +1,42 @@
-using System.Text.Json;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-
-namespace SIL.Harmony.Db.EntityConfig;
-
-public class CommitEntityConfig : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder.ToTable("Commits");
- builder.HasKey(c => c.Id);
- builder.ComplexProperty(c => c.HybridDateTime,
- hybridEntity =>
- {
- hybridEntity.Property(h => h.DateTime)
- .HasConversion(
- d => d.UtcDateTime,
- //need to use ticks here because the DateTime is stored as UTC, but the db records it as unspecified
- d => new DateTimeOffset(d.Ticks, TimeSpan.Zero))
- .HasColumnName("DateTime");
- hybridEntity.Property(h => h.Counter).HasColumnName("Counter");
- });
- builder.Property(c => c.Metadata)
- .HasColumnType("jsonb")
- .HasConversion(
- m => JsonSerializer.Serialize(m, (JsonSerializerOptions?)null),
- json => JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null) ?? new()
- );
- builder.HasMany(c => c.ChangeEntities)
- .WithOne()
- .HasForeignKey(c => c.CommitId);
- }
-}
+using System.Text.Json;
+using EFCore.ComplexIndexes;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace SIL.Harmony.Db.EntityConfig;
+
+public class CommitEntityConfig : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("Commits");
+ builder.HasKey(c => c.Id);
+ builder.ComplexProperty(c => c.HybridDateTime,
+ hybridEntity =>
+ {
+ hybridEntity.Property(h => h.DateTime)
+ .HasConversion(
+ d => d.UtcDateTime,
+ //need to use ticks here because the DateTime is stored as UTC, but the db records it as unspecified
+ d => new DateTimeOffset(d.Ticks, TimeSpan.Zero))
+ .HasColumnName("DateTime");
+ hybridEntity.Property(h => h.Counter).HasColumnName("Counter");
+ });
+ // Supports Harmony's default ordering (DateTime DESC, Counter DESC, Id DESC).
+ // EF Core 10 cannot express indexes mixing ComplexProperty members + scalars (efcore#11336, targeted for 11).
+ // We use EFCore.ComplexIndexes instead. The package doesn't support column direction,
+ // but an ASC index apparently works equivalently for reverse scans on SQLite, Postgres, and SQL Server.
+ builder.HasComplexCompositeIndex(
+ c => new { c.HybridDateTime.DateTime, c.HybridDateTime.Counter, c.Id },
+ indexName: "IX_Commits_DateTime_Counter_Id");
+ builder.Property(c => c.Metadata)
+ .HasColumnType("jsonb")
+ .HasConversion(
+ m => JsonSerializer.Serialize(m, (JsonSerializerOptions?)null),
+ json => JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null) ?? new()
+ );
+ builder.HasMany(c => c.ChangeEntities)
+ .WithOne()
+ .HasForeignKey(c => c.CommitId);
+ }
+}
diff --git a/src/SIL.Harmony/SIL.Harmony.csproj b/src/SIL.Harmony/SIL.Harmony.csproj
index d731f07..a2673ae 100644
--- a/src/SIL.Harmony/SIL.Harmony.csproj
+++ b/src/SIL.Harmony/SIL.Harmony.csproj
@@ -1,29 +1,30 @@
-
-
-
- SIL.Harmony
- true
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ SIL.Harmony
+ true
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+