Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
101 changes: 99 additions & 2 deletions SysML2.NET.Dal.Tests/AssemblerTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ArgumentNullException>());

Assert.That(() => this.assembler.Synchronize([]), Throws.Nothing);

Assert.That(this.assembler.Cache, Has.Count.EqualTo(0));

var dtos = new List<Core.DTO.Root.Elements.IElement>();

var packageDto = new SysML2.NET.Core.DTO.Kernel.Packages.Package
Expand Down Expand Up @@ -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<Core.DTO.Root.Elements.IElement>
{
private readonly List<Core.DTO.Root.Elements.IElement> 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<Core.DTO.Root.Elements.IElement> GetEnumerator()
{
this.EnumerationCount++;

return this.elements.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
}
}
77 changes: 53 additions & 24 deletions SysML2.NET.Dal/Assembler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,78 +54,107 @@ public Assembler(ILoggerFactory loggerFactory = null)
/// Gets the Cache that contains all the <see cref="Core.POCO.Root.Elements.IElement"/>s
/// </summary>
public ConcurrentDictionary<Guid, Lazy<Core.POCO.Root.Elements.IElement>> Cache { get; private set; }

/// <summary>
/// Synchronize the Cache based on the provided <paramref name="dtos"/>
/// </summary>
/// <param name="dtos">
/// the DTOs used to update the cache with
/// </param>
/// <remarks>
/// When <paramref name="dtos"/> 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.
/// </remarks>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="dtos"/> is null
/// </exception>
public void Synchronize(IEnumerable<Core.DTO.Root.Elements.IElement> dtos)
{
if (dtos == null)
{
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<Core.DTO.Root.Elements.IElement> ?? dtos.ToList();

var sw = Stopwatch.StartNew();

var deletedIdentifiers = new List<Guid>();

// 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<Core.POCO.Root.Elements.IElement> 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<Core.POCO.Root.Elements.IElement>(() => 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<Core.POCO.Root.Elements.IElement> 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();
}
}
}
Loading