From 20ed482f81536a897579f0fe6e5e693cd1ec7f09 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Thu, 12 Feb 2026 08:37:35 +0100 Subject: [PATCH 01/10] feature: add skipped aggregates to inventory part and runtime process data --- .../Inventories/InventoryProcessData.cs | 14 ++++++ .../Models/Inventories/InventoryPart.cs | 20 +++++++++ .../Services/Inventories/InventoryBuilder.cs | 1 + .../InventoryPartSkippedCountsTests.cs | 43 +++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 tests/ByteSync.Client.UnitTests/Models/Inventories/InventoryPartSkippedCountsTests.cs diff --git a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs index 441026522..3017d1bd3 100644 --- a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs +++ b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs @@ -13,6 +13,8 @@ public class InventoryProcessData : ReactiveObject { private readonly object _monitorDataLock = new object(); private readonly ConcurrentQueue _skippedEntries = new(); + private readonly ConcurrentDictionary _skippedCountsByReason = new(); + private int _skippedCount; public InventoryProcessData() { @@ -105,6 +107,8 @@ public List? Inventories public IReadOnlyCollection SkippedEntries => _skippedEntries.ToArray(); + public int SkippedCount => _skippedCount; + [Reactive] public DateTimeOffset InventoryStart { get; set; } @@ -141,6 +145,13 @@ public void Reset() public void RecordSkippedEntry(SkippedEntry entry) { _skippedEntries.Enqueue(entry); + _skippedCountsByReason.AddOrUpdate(entry.Reason, 1, (_, currentCount) => currentCount + 1); + Interlocked.Increment(ref _skippedCount); + } + + public int GetSkippedCountByReason(SkipReason reason) + { + return _skippedCountsByReason.TryGetValue(reason, out var count) ? count : 0; } public void SetError(Exception exception) @@ -166,6 +177,9 @@ private void ClearSkippedEntries() while (_skippedEntries.TryDequeue(out _)) { } + + _skippedCountsByReason.Clear(); + Interlocked.Exchange(ref _skippedCount, 0); } } diff --git a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs index 98683bcb3..f54aff09a 100644 --- a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs +++ b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs @@ -32,6 +32,16 @@ public InventoryPart(Inventory inventory, string rootPath, FileSystemTypes inven public List DirectoryDescriptions { get; set; } public bool IsIncompleteDueToAccess { get; set; } + + private Dictionary _skippedCountsByReason = new(); + + public Dictionary SkippedCountsByReason + { + get => _skippedCountsByReason; + set => _skippedCountsByReason = value ?? new Dictionary(); + } + + public int SkippedCount => SkippedCountsByReason.Values.Sum(); public string RootName { @@ -101,4 +111,14 @@ public void AddFileSystemDescription(FileSystemDescription fileSystemDescription DirectoryDescriptions.Add((DirectoryDescription) fileSystemDescription); } } + + public int GetSkippedCountByReason(SkipReason reason) + { + return SkippedCountsByReason.TryGetValue(reason, out var count) ? count : 0; + } + + public void RecordSkippedEntry(SkipReason reason) + { + SkippedCountsByReason[reason] = GetSkippedCountByReason(reason) + 1; + } } diff --git a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs index 3c03da85b..0c3d1664f 100644 --- a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs +++ b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs @@ -691,6 +691,7 @@ private void RecordSkippedEntry(InventoryPart inventoryPart, FileSystemInfo file DetectedKind = detectedKind }; + inventoryPart.RecordSkippedEntry(reason); InventoryProcessData.RecordSkippedEntry(entry); } diff --git a/tests/ByteSync.Client.UnitTests/Models/Inventories/InventoryPartSkippedCountsTests.cs b/tests/ByteSync.Client.UnitTests/Models/Inventories/InventoryPartSkippedCountsTests.cs new file mode 100644 index 000000000..4899cfd0c --- /dev/null +++ b/tests/ByteSync.Client.UnitTests/Models/Inventories/InventoryPartSkippedCountsTests.cs @@ -0,0 +1,43 @@ +using ByteSync.Models.Inventories; +using FluentAssertions; +using NUnit.Framework; + +namespace ByteSync.Client.UnitTests.Models.Inventories; + +[TestFixture] +public class InventoryPartSkippedCountsTests +{ + [Test] + public void RecordSkippedEntry_ShouldUpdateTotalAndReasonCounts() + { + // Arrange + var part = new InventoryPart(); + + // Act + part.RecordSkippedEntry(SkipReason.Hidden); + part.RecordSkippedEntry(SkipReason.Hidden); + part.RecordSkippedEntry(SkipReason.NoiseEntry); + + // Assert + part.SkippedCount.Should().Be(3); + part.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(2); + part.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1); + part.GetSkippedCountByReason(SkipReason.Offline).Should().Be(0); + } + + [Test] + public void SkippedCountsByReason_WhenSetToNull_ShouldFallbackToEmptyDictionary() + { + // Arrange + var part = new InventoryPart(); + part.SkippedCountsByReason = null!; + + // Act + var skippedCount = part.SkippedCount; + var hiddenCount = part.GetSkippedCountByReason(SkipReason.Hidden); + + // Assert + skippedCount.Should().Be(0); + hiddenCount.Should().Be(0); + } +} From 2aef375eddfe1a11c5e6b2b1e32b518034d3b4aa Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Thu, 12 Feb 2026 08:37:48 +0100 Subject: [PATCH 02/10] test: cover skipped aggregates in process data, builder and loader --- .../Inventories/InventoryProcessDataTests.cs | 37 +++++++++++++++++++ .../InventoryBuilderInspectorTests.cs | 8 ++++ .../InventoryLoaderIncompleteFlagTests.cs | 22 +++++++++++ 3 files changed, 67 insertions(+) diff --git a/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs index d0005a5b6..b15cd755e 100644 --- a/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs +++ b/tests/ByteSync.Client.UnitTests/Business/Inventories/InventoryProcessDataTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using ByteSync.Business.Inventories; +using ByteSync.Models.Inventories; using FluentAssertions; using NUnit.Framework; @@ -25,4 +26,40 @@ public void SetError_ShouldUpdateLastException_AndRaiseEvent() data.LastException.Should().Be(exception); values.Should().Contain(true); } + + [Test] + public void RecordSkippedEntry_ShouldUpdateGlobalAndReasonCounters() + { + // Arrange + var data = new InventoryProcessData(); + + // Act + data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.Hidden }); + data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.Hidden }); + data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.NoiseEntry }); + + // Assert + data.SkippedCount.Should().Be(3); + data.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(2); + data.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1); + data.GetSkippedCountByReason(SkipReason.Offline).Should().Be(0); + } + + [Test] + public void Reset_ShouldClearSkippedEntriesAndCounters() + { + // Arrange + var data = new InventoryProcessData(); + data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.Hidden }); + data.RecordSkippedEntry(new SkippedEntry { Reason = SkipReason.NoiseEntry }); + + // Act + data.Reset(); + + // Assert + data.SkippedEntries.Should().BeEmpty(); + data.SkippedCount.Should().Be(0); + data.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(0); + data.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(0); + } } diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs index 33d40338a..b0ce26cf4 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs @@ -267,8 +267,12 @@ public async Task Noise_Child_File_Is_Recorded() var invPath = Path.Combine(TestDirectory.FullName, "inv_noise_child.zip"); await builder.BuildBaseInventoryAsync(invPath); + var part = builder.Inventory.InventoryParts.Single(); + processData.SkippedEntries.Should() .ContainSingle(e => e.Name == "thumbs.db" && e.Reason == SkipReason.NoiseEntry); + part.SkippedCount.Should().Be(1); + part.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1); } [Test] @@ -306,6 +310,8 @@ public async Task Noise_Child_Directory_Is_Recorded_And_Not_Traversed() processData.SkippedEntries.Should() .ContainSingle(e => e.Name == "$RECYCLE.BIN" && e.Reason == SkipReason.NoiseEntry); + part.SkippedCount.Should().Be(1); + part.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1); } [Test] @@ -387,6 +393,8 @@ public async Task Offline_Root_File_Is_Recorded() part.FileDescriptions.Should().BeEmpty(); processData.SkippedEntries.Should() .ContainSingle(e => e.Name == "offline.txt" && e.Reason == SkipReason.Offline); + part.SkippedCount.Should().Be(1); + part.GetSkippedCountByReason(SkipReason.Offline).Should().Be(1); } [Test] diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs index 8f8b19cb7..81638399b 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs @@ -45,6 +45,28 @@ public void Load_ShouldMarkPartIncomplete_WhenInaccessibleDescriptionsExist() loadedInventory.InventoryParts[0].IsIncompleteDueToAccess.Should().BeTrue(); } + [Test] + public void Load_ShouldKeepSkippedCountsByReason() + { + // Arrange + var inventory = BuildInventoryWithInaccessibleDirectory(); + var part = inventory.InventoryParts[0]; + part.RecordSkippedEntry(SkipReason.Hidden); + part.RecordSkippedEntry(SkipReason.Hidden); + part.RecordSkippedEntry(SkipReason.NoiseEntry); + var zipPath = CreateInventoryZipFile(_tempDirectory, inventory); + + // Act + using var loader = new InventoryLoader(zipPath); + var loadedPart = loader.Inventory.InventoryParts[0]; + + // Assert + loadedPart.SkippedCount.Should().Be(3); + loadedPart.GetSkippedCountByReason(SkipReason.Hidden).Should().Be(2); + loadedPart.GetSkippedCountByReason(SkipReason.NoiseEntry).Should().Be(1); + loadedPart.GetSkippedCountByReason(SkipReason.Offline).Should().Be(0); + } + private static Inventory BuildInventoryWithInaccessibleDirectory() { var inventory = new Inventory From 1f83c03ffc611d86a549d0d3e3566273c970e707 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Thu, 12 Feb 2026 08:38:50 +0100 Subject: [PATCH 03/10] chroe: improve agents.md --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 9d27e9895..bb5b1d784 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,8 @@ This is a C#/.NET 8 solution with a client-server architecture: ### Overview This repository uses a branch-based development workflow. Please follow the conventions below when working in this project, especially when creating branches, writing PR titles, and generating commits or documentation. -Agents must not create branches until explicitly instructed to do so by a human. +Agents must not create branches until they have received clear instructions to do so from a human or an explicitly +invoked skill. ### Branch Naming Use the following prefixes: From a98347501ad1e3c7b0a648a992dbd7973e1d39dc Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Wed, 18 Feb 2026 16:40:14 +0100 Subject: [PATCH 04/10] refactor: cleanup --- .../Business/Inventories/InventoryProcessData.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs index 3017d1bd3..93e050755 100644 --- a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs +++ b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs @@ -151,9 +151,9 @@ public void RecordSkippedEntry(SkippedEntry entry) public int GetSkippedCountByReason(SkipReason reason) { - return _skippedCountsByReason.TryGetValue(reason, out var count) ? count : 0; + return _skippedCountsByReason.GetValueOrDefault(reason, 0); } - + public void SetError(Exception exception) { LastException = exception; @@ -181,5 +181,4 @@ private void ClearSkippedEntries() _skippedCountsByReason.Clear(); Interlocked.Exchange(ref _skippedCount, 0); } -} - +} \ No newline at end of file From 450f31ea9691a5d46edf78f5aad096d1b55201f2 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Wed, 18 Feb 2026 16:42:59 +0100 Subject: [PATCH 05/10] refactor: add comment --- src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs index 93e050755..db3317777 100644 --- a/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs +++ b/src/ByteSync.Client/Business/Inventories/InventoryProcessData.cs @@ -149,6 +149,7 @@ public void RecordSkippedEntry(SkippedEntry entry) Interlocked.Increment(ref _skippedCount); } + // should be used during issue 268 implementation public int GetSkippedCountByReason(SkipReason reason) { return _skippedCountsByReason.GetValueOrDefault(reason, 0); From fb1a88f3f8f58bd50770fdfcf174554315a1b9e4 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Wed, 18 Feb 2026 17:06:59 +0100 Subject: [PATCH 06/10] refactor: cleanup --- .../Models/Inventories/InventoryPart.cs | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs index f54aff09a..c5f0a7b78 100644 --- a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs +++ b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs @@ -11,38 +11,32 @@ public InventoryPart() FileDescriptions = new List(); DirectoryDescriptions = new List(); } - + public InventoryPart(Inventory inventory, string rootPath, FileSystemTypes inventoryPartType) : this() { Inventory = inventory; RootPath = rootPath; InventoryPartType = inventoryPartType; } - + public Inventory Inventory { get; set; } - + public string RootPath { get; set; } - + public FileSystemTypes InventoryPartType { get; set; } - + public string Code { get; set; } - + public List FileDescriptions { get; set; } - + public List DirectoryDescriptions { get; set; } - - public bool IsIncompleteDueToAccess { get; set; } - private Dictionary _skippedCountsByReason = new(); + public bool IsIncompleteDueToAccess { get; set; } - public Dictionary SkippedCountsByReason - { - get => _skippedCountsByReason; - set => _skippedCountsByReason = value ?? new Dictionary(); - } + public Dictionary SkippedCountsByReason { get; set; } = new Dictionary(); public int SkippedCount => SkippedCountsByReason.Values.Sum(); - + public string RootName { get @@ -52,63 +46,67 @@ public string RootName { case OSPlatforms.Windows: directorySeparatorChar = "\\"; + break; case OSPlatforms.Linux: case OSPlatforms.MacOs: directorySeparatorChar = "/"; + break; default: throw new ArgumentOutOfRangeException(nameof(directorySeparatorChar)); } - + return RootPath.Substring(RootPath.LastIndexOf(directorySeparatorChar, StringComparison.Ordinal)); } } - + protected bool Equals(InventoryPart other) { return Equals(Inventory, other.Inventory) && RootPath == other.RootPath && InventoryPartType == other.InventoryPartType; } - + public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((InventoryPart) obj); + + return Equals((InventoryPart)obj); } - + public override int GetHashCode() { unchecked { var hashCode = Inventory.GetHashCode(); hashCode = (hashCode * 397) ^ RootPath.GetHashCode(); - hashCode = (hashCode * 397) ^ (int) InventoryPartType; + hashCode = (hashCode * 397) ^ (int)InventoryPartType; + return hashCode; } } - + public override string ToString() { #if DEBUG return $"InventoryPart {RootName} {RootPath}"; #endif - + #pragma warning disable 162 return base.ToString(); #pragma warning restore 162 } - + public void AddFileSystemDescription(FileSystemDescription fileSystemDescription) { if (fileSystemDescription.FileSystemType == FileSystemTypes.File) { - FileDescriptions.Add((FileDescription) fileSystemDescription); + FileDescriptions.Add((FileDescription)fileSystemDescription); } else { - DirectoryDescriptions.Add((DirectoryDescription) fileSystemDescription); + DirectoryDescriptions.Add((DirectoryDescription)fileSystemDescription); } } @@ -121,4 +119,4 @@ public void RecordSkippedEntry(SkipReason reason) { SkippedCountsByReason[reason] = GetSkippedCountByReason(reason) + 1; } -} +} \ No newline at end of file From 5ee4165792746f0832d9aa66f931497a656507d5 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Wed, 18 Feb 2026 17:08:32 +0100 Subject: [PATCH 07/10] refactor: cleanup --- src/ByteSync.Client/Models/Inventories/InventoryPart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs index c5f0a7b78..c3fcb3d1f 100644 --- a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs +++ b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs @@ -33,7 +33,7 @@ public InventoryPart(Inventory inventory, string rootPath, FileSystemTypes inven public bool IsIncompleteDueToAccess { get; set; } - public Dictionary SkippedCountsByReason { get; set; } = new Dictionary(); + public Dictionary SkippedCountsByReason { get; set; } = new(); public int SkippedCount => SkippedCountsByReason.Values.Sum(); From 2a71845cdba1428e21f34f077ef5a586727cdc5b Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Wed, 18 Feb 2026 17:23:59 +0100 Subject: [PATCH 08/10] fix: fix null ref handling --- .../Models/Inventories/InventoryPart.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs index c3fcb3d1f..5dee9db23 100644 --- a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs +++ b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs @@ -6,6 +6,8 @@ namespace ByteSync.Models.Inventories; public class InventoryPart { + private Dictionary _skippedCountsByReason = new(); + public InventoryPart() { FileDescriptions = new List(); @@ -33,7 +35,13 @@ public InventoryPart(Inventory inventory, string rootPath, FileSystemTypes inven public bool IsIncompleteDueToAccess { get; set; } - public Dictionary SkippedCountsByReason { get; set; } = new(); + public Dictionary SkippedCountsByReason + { + get => _skippedCountsByReason; + + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + set => _skippedCountsByReason = value ?? new Dictionary(); + } public int SkippedCount => SkippedCountsByReason.Values.Sum(); From 5a8eac79c17f6403ec5bd3ca12e64f86c6f96eb0 Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Wed, 18 Feb 2026 17:25:13 +0100 Subject: [PATCH 09/10] refactor: improvements --- src/ByteSync.Client/Models/Inventories/InventoryPart.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs index 5dee9db23..84d866ce1 100644 --- a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs +++ b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs @@ -6,12 +6,13 @@ namespace ByteSync.Models.Inventories; public class InventoryPart { - private Dictionary _skippedCountsByReason = new(); + private Dictionary _skippedCountsByReason; public InventoryPart() { FileDescriptions = new List(); DirectoryDescriptions = new List(); + SkippedCountsByReason = new Dictionary(); } public InventoryPart(Inventory inventory, string rootPath, FileSystemTypes inventoryPartType) : this() From 524f6891e1c94548ddfc3af4c7793a5f50613b1c Mon Sep 17 00:00:00 2001 From: Paul Fresquet Date: Wed, 18 Feb 2026 17:32:20 +0100 Subject: [PATCH 10/10] refactor: cleanup --- src/ByteSync.Client/Models/Inventories/InventoryPart.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs index 84d866ce1..c2ebbe6bd 100644 --- a/src/ByteSync.Client/Models/Inventories/InventoryPart.cs +++ b/src/ByteSync.Client/Models/Inventories/InventoryPart.cs @@ -6,8 +6,6 @@ namespace ByteSync.Models.Inventories; public class InventoryPart { - private Dictionary _skippedCountsByReason; - public InventoryPart() { FileDescriptions = new List(); @@ -38,10 +36,10 @@ public InventoryPart(Inventory inventory, string rootPath, FileSystemTypes inven public Dictionary SkippedCountsByReason { - get => _skippedCountsByReason; + get; // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - set => _skippedCountsByReason = value ?? new Dictionary(); + set => field = value ?? new Dictionary(); } public int SkippedCount => SkippedCountsByReason.Values.Sum();