Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/1246 Allow reordering senses #1308

Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d8cb48b
Add collection position diffing
myieye Dec 3, 2024
62864b2
Add some collection diff tests and redesign to use after/before posit…
myieye Dec 4, 2024
80850a7
Exclude Order from external serialization, refactor, add more tests
myieye Dec 5, 2024
df2a15e
Update/extend entry tests for sense reordering and cleanup
myieye Dec 6, 2024
028203f
Rename method
myieye Dec 6, 2024
7df8aea
Don't set order on moved sense, because it shouldn't be necessary to …
myieye Dec 10, 2024
1726b5a
Set sense order when creating entries in bulk
myieye Dec 10, 2024
9d7b850
Standardize using AllSenses (not SensesOS) and fix moving between sub…
myieye Dec 10, 2024
c5f30ca
Cleanup
myieye Dec 10, 2024
970a43c
Fix tests
myieye Dec 10, 2024
b3bf2be
Revert making Sense partial
myieye Dec 10, 2024
5f5a492
Sort senses in place
myieye Dec 10, 2024
a87b7ba
Refactor OrderPicker to require siblings
myieye Dec 10, 2024
cebb3d5
Rename BetweenPosition props and simplify orderable methods
myieye Dec 10, 2024
b82d56b
Refactor collection diffing to use class/object for add, delete, move…
myieye Dec 10, 2024
4eda0bd
add back a comment which got lost
hahn-kev Dec 11, 2024
4085b10
simplify generating random moves for entry sync tests
hahn-kev Dec 11, 2024
01106a4
specify a namespace for the MiniLcmJson class
hahn-kev Dec 11, 2024
5ebbced
add comment about what the MiniLcmInternalAttribute.cs is used for
hahn-kev Dec 11, 2024
490ac23
Fix failing test
myieye Dec 12, 2024
7c9450b
Only pass senseId to MoveSense
myieye Dec 12, 2024
121ebf3
Use letters for test glosses, because orders use int
myieye Dec 12, 2024
59b1db5
Don't mock IOrderable in tests
myieye Dec 12, 2024
68587bf
Revert unintended extra sync
myieye Dec 12, 2024
eabc7a7
Fix SetOrderChange type discriminator(s)
myieye Dec 12, 2024
1e4d485
Optimize order picker
myieye Dec 13, 2024
ea86354
Refactor Orderable diffing with custom diff formatter
myieye Dec 13, 2024
72d170a
Merge remote-tracking branch 'origin/develop' into feat/1246-allow-re…
myieye Dec 13, 2024
42a0418
Incorporated PR feedback
myieye Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("update-entry-test", "en", "en"));
}

protected override bool ApiUsesImplicitOrdering => true;
}
70 changes: 64 additions & 6 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections.Frozen;
using System.Globalization;
using System.Reflection;
using System.Text;
using FluentValidation;
using FwDataMiniLcmBridge.Api.UpdateProxy;
Expand Down Expand Up @@ -206,7 +205,7 @@
}
await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",
"Revert WritingSystem",
async () =>

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 208 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var updateProxy = new UpdateWritingSystemProxy(lcmWritingSystem, this)
{
Expand Down Expand Up @@ -649,7 +648,7 @@
var entries = GetEntries(e =>
e.CitationForm.SearchValue(query) ||
e.LexemeFormOA.Form.SearchValue(query) ||
e.SensesOS.Any(s => s.Gloss.SearchValue(query)), options);
e.AllSenses.Any(s => s.Gloss.SearchValue(query)), options);
return entries;
}

Expand Down Expand Up @@ -874,9 +873,10 @@
return Task.CompletedTask;
}

internal void CreateSense(ILexEntry lexEntry, Sense sense)
internal void CreateSense(ILexEntry lexEntry, Sense sense, BetweenPosition? between = null)
{
var lexSense = LexSenseFactory.Create(sense.Id, lexEntry);
var lexSense = LexSenseFactory.Create(sense.Id);
InsertSense(lexEntry, lexSense, between);
var msa = new SandboxGenericMSA() { MsaType = lexSense.GetDesiredMsaType() };
if (sense.PartOfSpeechId.HasValue && PartOfSpeechRepository.TryGetObject(sense.PartOfSpeechId.Value, out var pos))
{
Expand All @@ -886,6 +886,46 @@
ApplySenseToLexSense(sense, lexSense);
}

internal void InsertSense(ILexEntry lexEntry, ILexSense lexSense, BetweenPosition? between = null)
{
var previousSenseId = between?.Previous;
var nextSenseId = between?.Next;

var previousSense = previousSenseId.HasValue ? lexEntry.AllSenses.Find(s => s.Guid == previousSenseId) : null;
if (previousSense is not null)
{
if (previousSense.SensesOS.Count > 0)
{
// if the sense has sub-senses, our sense will only come directly after it if it is the first sub-sense
previousSense.SensesOS.Insert(0, lexSense);
}
else
{
// todo the user might have wanted it to be a subsense of previousSense
var allSiblings = previousSense.Owner == lexEntry ? lexEntry.SensesOS
: previousSense.Owner is ILexSense parentSense ? parentSense.SensesOS
: throw new InvalidOperationException("Sense parent is not a sense or the expected entry");
var insertI = allSiblings.IndexOf(previousSense) + 1;
lexEntry.SensesOS.Insert(insertI, lexSense);
}
return;
}

var nextSense = nextSenseId.HasValue ? lexEntry.AllSenses.Find(s => s.Guid == nextSenseId) : null;
if (nextSense is not null)
{
// todo the user might have wanted it to be a subsense of whatever is before nextSense
var allSiblings = nextSense.Owner == lexEntry ? lexEntry.SensesOS
: nextSense.Owner is ILexSense parentSense ? parentSense.SensesOS
: throw new InvalidOperationException("Sense parent is not a sense or the expected entry");
var insertI = allSiblings.IndexOf(nextSense);
lexEntry.SensesOS.Insert(insertI, lexSense);
return;
}

lexEntry.SensesOS.Add(lexSense);
}

private void ApplySenseToLexSense(Sense sense, ILexSense lexSense)
{
if (lexSense.MorphoSyntaxAnalysisRA.GetPartOfSpeech()?.Guid != sense.PartOfSpeechId)
Expand Down Expand Up @@ -917,15 +957,15 @@
return Task.FromResult(lcmSense is null ? null : FromLexSense(lcmSense));
}

public Task<Sense> CreateSense(Guid entryId, Sense sense)
public Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
{
if (sense.Id == default) sense.Id = Guid.NewGuid();
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Sense",
"Remove sense",
Cache.ServiceLocator.ActionHandler,
() => CreateSense(lexEntry, sense));
() => CreateSense(lexEntry, sense, between));
return Task.FromResult(FromLexSense(SenseRepository.GetObject(sense.Id)));
}

Expand Down Expand Up @@ -955,6 +995,24 @@
return await GetSense(entryId, after.Id) ?? throw new NullReferenceException("unable to find sense with id " + after.Id);
}

public Task<Sense> MoveSense(Guid entryId, Sense sense, BetweenPosition between)
{
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
if (!SenseRepository.TryGetObject(sense.Id, out var lexSense))
throw new InvalidOperationException("Sense not found");

UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Move Sense",
"Move Sense back",
Cache.ServiceLocator.ActionHandler,
() =>
{
// LibLCM treats an insert as a move if the sense is already in the entry
InsertSense(lexEntry, lexSense, between);
});
return Task.FromResult(FromLexSense(lexSense));
}

public Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Add Semantic Domain to Sense",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,9 @@ public override MultiString LiteralMeaning
set => throw new NotImplementedException();
}

public override IList<Sense> Senses
public override List<Sense> Senses
{
get =>
new UpdateListProxy<Sense>(
sense => _lexboxLcmApi.CreateSense(_lcmEntry, sense),
sense => _lexboxLcmApi.DeleteSense(Id, sense.Id),
i => new UpdateSenseProxy(_lcmEntry.SensesOS[i], _lexboxLcmApi),
_lcmEntry.SensesOS.Count
);
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}

Expand Down
24 changes: 22 additions & 2 deletions backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
using FwLiteProjectSync.Tests.Fixtures;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;
using MiniLcm.Tests;
using MiniLcm.Tests.AutoFakerHelpers;
using Soenneker.Utils.AutoBogus;
using Soenneker.Utils.AutoBogus.Config;

namespace FwLiteProjectSync.Tests;

public class EntrySyncTests : IClassFixture<SyncFixture>
{
private static readonly AutoFaker AutoFaker = new(builder => builder.WithOverride(new MultiStringOverride()).WithOverride(new ObjectWithIdOverride()));
private static readonly AutoFaker AutoFaker = new(new AutoFakerConfig()
{
RepeatCount = 5,
Overrides =
[
new MultiStringOverride(),
new ObjectWithIdOverride(),
new OrderableOverride(),
]
});

public EntrySyncTests(SyncFixture fixture)
{
_fixture = fixture;
Expand All @@ -22,6 +32,16 @@ public async Task CanSyncRandomEntries()
{
var createdEntry = await _fixture.CrdtApi.CreateEntry(await AutoFaker.EntryReadyForCreation(_fixture.CrdtApi));
var after = await AutoFaker.EntryReadyForCreation(_fixture.CrdtApi, entryId: createdEntry.Id);

// copy some senses over, so moves happen
List<Sense> createdSenses = [.. createdEntry.Senses];
for (var i = 0; i < Random.Shared.Next(AutoFaker.Config.RepeatCount); i++)
{
var copy = createdSenses[Random.Shared.Next(createdSenses.Count - 1)];
createdSenses.Remove(copy); // we don't want duplicates
after.Senses.Insert(Random.Shared.Next(after.Senses.Count), copy);
}

await EntrySync.Sync(after, createdEntry, _fixture.CrdtApi);
var actual = await _fixture.CrdtApi.GetEntry(after.Id);
actual.Should().NotBeNull();
Expand Down
4 changes: 3 additions & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ private void ShouldAllBeEquivalentTo(Dictionary<Guid, Entry> crdtEntries, Dictio
crdtEntry.Should().BeEquivalentTo(fwdataEntry,
options => options
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id),
.For(e => e.ComplexForms).Exclude(c => c.Id)
.For(e => e.Senses).Exclude(s => s.Order),
$"CRDT entry {crdtEntry.Id} was synced with FwData");
}
}
Expand Down Expand Up @@ -150,6 +151,7 @@ public async Task SyncWithoutImport_CrdtShouldMatchFwdata()
[Fact]
public async Task SecondSena3SyncDoesNothing()
{
await _syncService.Sync(_crdtApi, _fwDataApi);
myieye marked this conversation as resolved.
Show resolved Hide resolved
await _syncService.Sync(_crdtApi, _fwDataApi);
var secondSync = await _syncService.Sync(_crdtApi, _fwDataApi);
secondSync.CrdtChanges.Should().Be(0);
Expand Down
25 changes: 19 additions & 6 deletions backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ public async Task FirstSyncJustDoesAnImport()
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down Expand Up @@ -145,7 +147,9 @@ await crdtApi.CreateEntry(new Entry()
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down Expand Up @@ -223,7 +227,9 @@ public async Task CreatingAComplexEntryInFwDataSyncsWithoutIssue()
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));

// Sync again, ensure no problems or changes
Expand Down Expand Up @@ -305,7 +311,9 @@ await crdtApi.CreateEntry(new Entry()
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down Expand Up @@ -384,7 +392,9 @@ await crdtApi.CreateEntry(new Entry()
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand All @@ -408,6 +418,7 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth()
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
//todo the headword should be changed
.For(e => e.Components).Exclude(c => c.ComponentHeadword)
Expand Down Expand Up @@ -475,7 +486,9 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth()
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down
11 changes: 8 additions & 3 deletions backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using FwLiteProjectSync.Tests.Fixtures;
using MiniLcm.Models;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;
using MiniLcm.Tests.AutoFakerHelpers;
using Soenneker.Utils.AutoBogus;
Expand All @@ -11,7 +10,13 @@ public class UpdateDiffTests
{
private static readonly AutoFaker AutoFaker = new(new AutoFakerConfig()
{
Overrides = [new MultiStringOverride(), new WritingSystemIdOverride()]
RepeatCount = 5,
Overrides =
[
new MultiStringOverride(),
new WritingSystemIdOverride(),
new OrderableOverride(),
]
});

[Fact]
Expand Down
11 changes: 9 additions & 2 deletions backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using MiniLcm;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;

namespace FwLiteProjectSync;

Expand Down Expand Up @@ -195,9 +196,9 @@ public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId)
return api.GetSense(entryId, id);
}

public Task<Sense> CreateSense(Guid entryId, Sense sense)
public Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? position = null)
{
DryRunRecords.Add(new DryRunRecord(nameof(CreateSense), $"Create sense {sense.Gloss}"));
DryRunRecords.Add(new DryRunRecord(nameof(CreateSense), $"Create sense {sense.Gloss} between {position?.Previous} and {position?.Next}"));
return Task.FromResult(sense);
}

Expand All @@ -218,6 +219,12 @@ public async Task<Sense> UpdateSense(Guid entryId, Sense before, Sense after)
return await GetSense(entryId, after.Id) ?? throw new NullReferenceException($"unable to find sense with id {after.Id}");
}

public Task<Sense> MoveSense(Guid entryId, Sense sense, BetweenPosition between)
{
DryRunRecords.Add(new DryRunRecord(nameof(MoveSense), $"Move sense {sense.Gloss} between {between.Previous} and {between.Next}"));
return Task.FromResult(sense);
}

public Task DeleteSense(Guid entryId, Guid senseId)
{
DryRunRecords.Add(new DryRunRecord(nameof(DeleteSense), $"Delete sense {senseId}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@
{
DerivedType: CreateComplexFormType,
TypeDiscriminator: CreateComplexFormType
},
{
DerivedType: SetOrderChange<Sense>,
TypeDiscriminator: SetOrderChange`1
myieye marked this conversation as resolved.
Show resolved Hide resolved
}
],
IgnoreUnrecognizedTypeDiscriminators: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
Navigations:
ComplexForms (IList<ComplexFormComponent>) Collection ToDependent ComplexFormComponent
Components (IList<ComplexFormComponent>) Collection ToDependent ComplexFormComponent
Senses (IList<Sense>) Collection ToDependent Sense
Senses (List<Sense>) Collection ToDependent Sense
Keys:
Id PK
Foreign keys:
Expand Down Expand Up @@ -200,6 +200,7 @@
Gloss (MultiString) Required
Annotations:
Relational:ColumnType: jsonb
Order (double) Required
PartOfSpeech (string) Required
PartOfSpeechId (Guid?)
SemanticDomains (IList<SemanticDomain>) Required
Expand Down
10 changes: 8 additions & 2 deletions backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ namespace LcmCrdt.Tests;

public class DataModelSnapshotTests : IAsyncLifetime
{
private static readonly AutoFaker Faker = new AutoFaker(new AutoFakerConfig()
private static readonly AutoFaker Faker = new(new AutoFakerConfig()
{
Overrides = [new MultiStringOverride(), new WritingSystemIdOverride()]
RepeatCount = 5,
Overrides =
[
new MultiStringOverride(),
new WritingSystemIdOverride(),
new OrderableOverride(),
]
});

protected readonly AsyncServiceScope _services;
Expand Down
Loading
Loading