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..817585ff6 100644 --- a/SysML2.NET.Dal/Assembler.cs +++ b/SysML2.NET.Dal/Assembler.cs @@ -54,13 +54,20 @@ 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 /// /// /// 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,86 @@ 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 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); + + 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) { - 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 (this.logger.IsEnabled(LogLevel.Trace)) { - 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 (this.logger.IsEnabled(LogLevel.Trace)) + { + this.logger.LogTrace("{PocoType}:{Identifier} added to Cache", poco.GetType().Name, poco.Id); + } + } + + 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); } - this.logger.LogDebug("A total of {0} POCOs have been added to the Cache in {1} [ms]", newIdentifiers.Count(), 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); + + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("POCO reference properties updated in {Elapsed} [ms]", sw.ElapsedMilliseconds); + } + + sw.Stop(); } } }