From b0b81ee4acab35065114ebee452fc1aab16c454a Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 25 Aug 2026 12:23:07 +0200 Subject: [PATCH 1/2] fix #347 --- SysML2.NET.Dal.Tests/AssemblerTestFixture.cs | 101 ++++++++++++++++++- SysML2.NET.Dal/Assembler.cs | 68 ++++++++----- 2 files changed, 144 insertions(+), 25 deletions(-) diff --git a/SysML2.NET.Dal.Tests/AssemblerTestFixture.cs b/SysML2.NET.Dal.Tests/AssemblerTestFixture.cs index 330e41700..e4e08e27e 100644 --- a/SysML2.NET.Dal.Tests/AssemblerTestFixture.cs +++ b/SysML2.NET.Dal.Tests/AssemblerTestFixture.cs @@ -21,8 +21,11 @@ namespace SysML2.NET.Dal.Tests { using System; + using System.Collections; using System.Collections.Generic; - + using System.Globalization; + using System.Linq; + using NUnit.Framework; using SysML2.NET.Dal; @@ -44,8 +47,14 @@ public void Setup() } [Test] - public void Verify_that_synchronize_Works_as_Expected() + public void VerifySynchronize() { + Assert.That(() => this.assembler.Synchronize(null), Throws.TypeOf()); + + Assert.That(() => this.assembler.Synchronize([]), Throws.Nothing); + + Assert.That(this.assembler.Cache, Has.Count.EqualTo(0)); + var dtos = new List(); var packageDto = new SysML2.NET.Core.DTO.Kernel.Packages.Package @@ -122,5 +131,93 @@ public void Verify_that_synchronize_Works_as_Expected() Assert.That(featurePoco.DeclaredName, Is.EqualTo("some updated feature")); } + + [Test] + public void Synchronize_WithDuplicateIdentifiers_KeepsTheFirstDto() + { + var identifier = Guid.Parse("4a2e6c2e-3d6b-4a1f-9a6f-7f0d1a2b3c4d"); + + var packageDto = new Core.DTO.Kernel.Packages.Package + { + Id = identifier, + DeclaredName = "the first package", + ElementId = identifier.ToString() + }; + + var duplicateDto = new Core.DTO.Kernel.Packages.Package + { + Id = identifier, + DeclaredName = "the duplicate package", + ElementId = identifier.ToString() + }; + + Assert.That(() => this.assembler.Synchronize([packageDto, duplicateDto]), Throws.Nothing); + + Core.POCO.Kernel.Packages.Package packagePoco = null; + + if (this.assembler.Cache.TryGetValue(identifier, out this.lazyPoco)) + { + packagePoco = (Core.POCO.Kernel.Packages.Package)this.lazyPoco.Value; + } + + using (Assert.EnterMultipleScope()) + { + Assert.That(this.assembler.Cache, Has.Count.EqualTo(1)); + Assert.That(packagePoco.DeclaredName, Is.EqualTo("the first package")); + } + } + + [Test] + public void Synchronize_WithGrowingDtoSequence_DoesNotRescanTheSequencePerElement() + { + var smallModel = new EnumerationCountingElements(64); + var largeModel = new EnumerationCountingElements(1024); + + var smallAssembler = new Assembler(); + var largeAssembler = new Assembler(); + + smallAssembler.Synchronize(smallModel); + largeAssembler.Synchronize(largeModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(smallAssembler.Cache, Has.Count.EqualTo(smallModel.Count)); + Assert.That(largeAssembler.Cache, Has.Count.EqualTo(largeModel.Count)); + Assert.That(smallModel.EnumerationCount, Is.LessThanOrEqualTo(4)); + Assert.That(largeModel.EnumerationCount, Is.EqualTo(smallModel.EnumerationCount)); + } + } + + private sealed class EnumerationCountingElements : IReadOnlyList + { + private readonly List elements; + + public EnumerationCountingElements(int size) + { + this.elements = Enumerable.Range(0, size) + .Select(index => (Core.DTO.Root.Elements.IElement)new Core.DTO.Kernel.Packages.Package + { + Id = Guid.NewGuid(), + DeclaredName = $"package {index.ToString(CultureInfo.InvariantCulture)}", + ElementId = index.ToString(CultureInfo.InvariantCulture) + }) + .ToList(); + } + + public int EnumerationCount { get; private set; } + + public int Count => this.elements.Count; + + public Core.DTO.Root.Elements.IElement this[int index] => this.elements[index]; + + public IEnumerator GetEnumerator() + { + this.EnumerationCount++; + + return this.elements.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } } } diff --git a/SysML2.NET.Dal/Assembler.cs b/SysML2.NET.Dal/Assembler.cs index 0971ade9e..5713929df 100644 --- a/SysML2.NET.Dal/Assembler.cs +++ b/SysML2.NET.Dal/Assembler.cs @@ -61,6 +61,13 @@ public Assembler(ILoggerFactory loggerFactory = null) /// /// the DTOs used to update the cache with /// + /// + /// When carries more than one DTO with the same identifier, the first occurrence + /// is the one that is added to the Cache and every subsequent duplicate is ignored. + /// + /// + /// Thrown when is null + /// public void Synchronize(IEnumerable dtos) { if (dtos == null) @@ -68,64 +75,79 @@ public void Synchronize(IEnumerable dtos) throw new ArgumentNullException(nameof(dtos), $"The {nameof(dtos)} may not be null"); } + // the DTOs are walked three times, materialize once so that a lazy sequence is not re-evaluated on every pass + var elements = dtos as IReadOnlyList ?? dtos.ToList(); + + var isTraceEnabled = this.logger.IsEnabled(LogLevel.Trace); + var sw = Stopwatch.StartNew(); var deletedIdentifiers = new List(); // update all POCOs based on provided DTOs, the result is a list unique identifiers of objects that may be removed this.logger.LogDebug("Update Value properties of POCO and Removed deleted Reference Properties"); - foreach (var dto in dtos) + + foreach (var dto in elements) { if (this.Cache.TryGetValue(dto.Id, out var lazyPoco)) { - var poco = lazyPoco.Value; - var deletedPocos = poco.UpdateValueAndRemoveDeletedReferenceProperties(dto).ToList(); - deletedIdentifiers.AddRange(deletedPocos); + deletedIdentifiers.AddRange(lazyPoco.Value.UpdateValueAndRemoveDeletedReferenceProperties(dto)); } } - this.logger.LogDebug("A total of {0} identifiers have been processed in {1} [ms] and ready to be deleted", deletedIdentifiers, sw.ElapsedMilliseconds); + + this.logger.LogDebug("A total of {DeletedCount} identifiers have been processed in {Elapsed} [ms] and ready to be deleted", deletedIdentifiers.Count, sw.ElapsedMilliseconds); // removed POCOs that are up for deletion foreach (var identifier in deletedIdentifiers) { - Lazy lazyPoco; - if (this.Cache.TryRemove(identifier, out lazyPoco)) + if (!this.Cache.TryRemove(identifier, out var deletedLazyPoco)) { - this.logger.LogTrace("{0} with identifier {1} was deleted", lazyPoco.Value.GetType().Name, identifier); + this.logger.LogWarning("The element with identifier {Identifier} was not deleted as it could not be found in the cache", identifier); + continue; } - else + + if (isTraceEnabled) { - this.logger.LogWarning("{0} with identifier {1} was not deleted as it could not be found in the cache", lazyPoco.Value.GetType().Name, identifier); + this.logger.LogTrace("{PocoType} with identifier {Identifier} was deleted", deletedLazyPoco.Value.GetType().Name, identifier); } } sw.Restart(); this.logger.LogDebug("Add new POCOs to dictionary based on DTOs"); + var elementFactory = new ElementFactory(); - var existingIdentifiers = this.Cache.Keys.ToList(); - var dtoIdentifiers = dtos.Select(x => x.Id).ToList(); - var newIdentifiers = dtoIdentifiers.Except(existingIdentifiers); - foreach (var identifier in newIdentifiers) + var addedCount = 0; + + foreach (var dto in elements.Where(element => !this.Cache.ContainsKey(element.Id))) { - var dto = dtos.Single(x => x.Id == identifier); var poco = elementFactory.Create(dto); + this.Cache.AddOrUpdate(poco.Id, new Lazy(() => poco), (key, oldValue) => oldValue); - this.logger.LogTrace("{0}:{1} added to Cache", poco.GetType().Name, poco.Id); + + addedCount++; + + if (isTraceEnabled) + { + this.logger.LogTrace("{PocoType}:{Identifier} added to Cache", poco.GetType().Name, poco.Id); + } } - this.logger.LogDebug("A total of {0} POCOs have been added to the Cache in {1} [ms]", newIdentifiers.Count(), sw.ElapsedMilliseconds); + + this.logger.LogDebug("A total of {AddedCount} POCOs have been added to the Cache in {Elapsed} [ms]", addedCount, sw.ElapsedMilliseconds); sw.Restart(); this.logger.LogDebug("Update POCO reference properties"); - foreach (var dto in dtos) + + foreach (var dto in elements) { - Lazy lazyPoco; - if (this.Cache.TryGetValue(dto.Id, out lazyPoco)) + if (this.Cache.TryGetValue(dto.Id, out var lazyPoco)) { - var poco = lazyPoco.Value; - poco.UpdateReferenceProperties(dto, this.Cache); + lazyPoco.Value.UpdateReferenceProperties(dto, this.Cache); } } - this.logger.LogDebug("POCO reference properties updated in {0} [ms]", sw.ElapsedMilliseconds); + + this.logger.LogDebug("POCO reference properties updated in {Elapsed} [ms]", sw.ElapsedMilliseconds); + + sw.Stop(); } } } From 52d2bac271c777b76042adc73aec6858b1c87347 Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 25 Aug 2026 12:41:24 +0200 Subject: [PATCH 2/2] Fix SQ issues --- SysML2.NET.Dal/Assembler.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/SysML2.NET.Dal/Assembler.cs b/SysML2.NET.Dal/Assembler.cs index 5713929df..817585ff6 100644 --- a/SysML2.NET.Dal/Assembler.cs +++ b/SysML2.NET.Dal/Assembler.cs @@ -54,7 +54,7 @@ public Assembler(ILoggerFactory loggerFactory = null) /// Gets the Cache that contains all the s /// public ConcurrentDictionary> Cache { get; private set; } - + /// /// Synchronize the Cache based on the provided /// @@ -78,8 +78,6 @@ public void Synchronize(IEnumerable dtos) // the DTOs are walked three times, materialize once so that a lazy sequence is not re-evaluated on every pass var elements = dtos as IReadOnlyList ?? dtos.ToList(); - var isTraceEnabled = this.logger.IsEnabled(LogLevel.Trace); - var sw = Stopwatch.StartNew(); var deletedIdentifiers = new List(); @@ -95,7 +93,10 @@ public void Synchronize(IEnumerable dtos) } } - this.logger.LogDebug("A total of {DeletedCount} identifiers have been processed in {Elapsed} [ms] and ready to be deleted", deletedIdentifiers.Count, sw.ElapsedMilliseconds); + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("A total of {DeletedCount} identifiers have been processed in {Elapsed} [ms] and ready to be deleted", deletedIdentifiers.Count, sw.ElapsedMilliseconds); + } // removed POCOs that are up for deletion foreach (var identifier in deletedIdentifiers) @@ -106,7 +107,7 @@ public void Synchronize(IEnumerable dtos) continue; } - if (isTraceEnabled) + if (this.logger.IsEnabled(LogLevel.Trace)) { this.logger.LogTrace("{PocoType} with identifier {Identifier} was deleted", deletedLazyPoco.Value.GetType().Name, identifier); } @@ -126,13 +127,16 @@ public void Synchronize(IEnumerable dtos) addedCount++; - if (isTraceEnabled) + if (this.logger.IsEnabled(LogLevel.Trace)) { this.logger.LogTrace("{PocoType}:{Identifier} added to Cache", poco.GetType().Name, poco.Id); } } - this.logger.LogDebug("A total of {AddedCount} POCOs have been added to the Cache in {Elapsed} [ms]", addedCount, sw.ElapsedMilliseconds); + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("A total of {AddedCount} POCOs have been added to the Cache in {Elapsed} [ms]", addedCount, sw.ElapsedMilliseconds); + } sw.Restart(); this.logger.LogDebug("Update POCO reference properties"); @@ -145,7 +149,10 @@ public void Synchronize(IEnumerable dtos) } } - this.logger.LogDebug("POCO reference properties updated in {Elapsed} [ms]", sw.ElapsedMilliseconds); + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("POCO reference properties updated in {Elapsed} [ms]", sw.ElapsedMilliseconds); + } sw.Stop(); }