Skip to content

Commit d936bcb

Browse files
committed
Merge branch 'improvement/TrackedRecordCollection.Remove-handling' into develop
Previously, calling `Remove()` and `Clear()` on `TrackedRecordCollection<>` and its derivatives `AttributeCollection` and `TopicReferenceCollection` would bypass any business logic on associated properties. It was also inconsistent with `SetValue()` which would accept null values, and correctly enforce business logic, but would keep the `TrackedRecord<>` in place, thus causing potential confusion and bugs (since e.g. `Contains()` would report that the `Key` existed, even though it had a null `Value`). To mitigate this, the `SetValue()` logic was updated to call `Remove()` if the `Value` was null (3d1ad6b), `Remove()` was updated to enforce business logic (0bccf1f), and `Clear()` was updated to call `Remove()` (abfe213)—thus enforcing business logic on each individual item. In addition to updating existing tests which assumed that `SetValue()` wouldn't delete a record (7aae9bc), I also introduced a _new_ unit test to validate that `Clear()` (and, thus, `Remove()`) will, in fact, enforce business logic (fa08bfb). This satisfies the requirements for #79.
2 parents 706b658 + fa08bfb commit d936bcb

5 files changed

Lines changed: 61 additions & 37 deletions

File tree

OnTopic.Tests/AttributeCollectionTest.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,26 @@ public void SetValue_ValueChanged_IsDirty() {
439439

440440
}
441441

442+
/*==========================================================================================================================
443+
| TEST: CLEAR: NON-NULLABLE VALUE WITH BUSINESS LOGIC: THROWS EXCEPTION
444+
\-------------------------------------------------------------------------------------------------------------------------*/
445+
/// <summary>
446+
/// Sets an attribute on a topic instance with an valid value, and then attempts to clear all attributes. Ensures that the
447+
/// business logic is enforced by throwing an exception on the non-nullable property.
448+
/// </summary>
449+
[Fact]
450+
public void Clear_NonNullableValueWithBusinessLogic_ThrowsException() {
451+
452+
var topic = new CustomTopic("Test", "Page");
453+
454+
topic.NonNullableAttribute = "Test";
455+
456+
Assert.Throws<ArgumentNullException>(() =>
457+
topic.Attributes.Clear()
458+
);
459+
460+
}
461+
442462
/*==========================================================================================================================
443463
| TEST: CLEAR: EXISTING VALUES: IS DIRTY?
444464
\-------------------------------------------------------------------------------------------------------------------------*/

OnTopic.Tests/Entities/CustomTopic.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ public int NumericAttribute {
8383
}
8484
}
8585

86+
/*==========================================================================================================================
87+
| NON-NULLABLE ATTRIBUTE
88+
\-------------------------------------------------------------------------------------------------------------------------*/
89+
/// <summary>
90+
/// Provides a property which does not permit null values as a means of ensuring the business logic is enforced when
91+
/// removing values.
92+
/// </summary>
93+
[AttributeSetter]
94+
public string NonNullableAttribute {
95+
get => Attributes.GetValue("NonNullableAttribute")?? "";
96+
set {
97+
Contract.Requires(value, nameof(value));
98+
SetAttributeValue("NonNullableAttribute", value.ToString(CultureInfo.InvariantCulture));
99+
}
100+
}
101+
86102
/*==========================================================================================================================
87103
| DATE/TIME ATTRIBUTE
88104
\-------------------------------------------------------------------------------------------------------------------------*/

OnTopic.Tests/TopicReferenceCollectionTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public void SetValue_ExistingReference_TopicUpdated() {
220220
/// TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)"/>, updates the
221221
/// reference using <see cref="TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?,
222222
/// DateTime?)"/> with a <c>null</c> value, and confirms that the <see cref="Topic"/> reference and <see cref="Topic.
223-
/// IncomingRelationships"/> are correctly updated.
223+
/// IncomingRelationships"/> are correctly removed.
224224
/// </summary>
225225
/// <remarks>
226226
/// This calls <see cref="TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)
@@ -238,7 +238,7 @@ public void SetValue_ExistingReference_IncomingRelationshipsUpdates() {
238238
topic.References.SetValue("Reference", null);
239239
topic.References.SetValue("Reference", null);
240240

241-
Assert.True(topic.References.Contains("Reference"));
241+
Assert.False(topic.References.Contains("Reference"));
242242
Assert.Null(topic.References.GetValue("Reference"));
243243
Assert.Equal<int?>(0, reference.IncomingRelationships.GetValues("Reference")?.Count);
244244

@@ -262,7 +262,7 @@ public void SetValue_NullReference_TopicRemoved() {
262262
topic.References.SetValue("Reference", reference);
263263
topic.References.SetValue("Reference", null);
264264

265-
Assert.Single(topic.References);
265+
Assert.Empty(topic.References);
266266
Assert.Null(topic.References.GetValue("Reference"));
267267

268268
}

OnTopic/Associations/TopicReferenceCollection.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -135,25 +135,5 @@ protected override sealed void RemoveItem(int index) {
135135

136136
}
137137

138-
/*==========================================================================================================================
139-
| OVERRIDE: CLEAR ITEMS
140-
\-------------------------------------------------------------------------------------------------------------------------*/
141-
/// <inheritdoc/>
142-
protected override sealed void ClearItems() {
143-
144-
/*------------------------------------------------------------------------------------------------------------------------
145-
| Handle recipricol references
146-
\-----------------------------------------------------------------------------------------------------------------------*/
147-
foreach (var item in Items) {
148-
item.Value?.IncomingRelationships.Remove(item.Key, AssociatedTopic, true);
149-
}
150-
151-
/*------------------------------------------------------------------------------------------------------------------------
152-
| Provide base logic
153-
\-----------------------------------------------------------------------------------------------------------------------*/
154-
base.ClearItems();
155-
156-
}
157-
158138
} //Class
159139
} //Namespace

OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ internal void SetValue(
433433

434434
/*------------------------------------------------------------------------------------------------------------------------
435435
| Update from business logic
436-
>-----------------------------------------------------------------------------------------------------------------------
436+
>-------------------------------------------------------------------------------------------------------------------------
437437
| If the original values have already been applied, and SetValue() is being triggered a second time after enforcing
438438
| business logic, then use the original values, while applying any change in the value triggered by the business logic.
439439
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -447,7 +447,7 @@ internal void SetValue(
447447

448448
/*------------------------------------------------------------------------------------------------------------------------
449449
| Update existing item
450-
>-----------------------------------------------------------------------------------------------------------------------
450+
>-------------------------------------------------------------------------------------------------------------------------
451451
| Because TrackedRecord<T> is immutable, a new instance must be constructed to replace the previous version.
452452
\-----------------------------------------------------------------------------------------------------------------------*/
453453
else if (originalItem is not null) {
@@ -472,10 +472,11 @@ internal void SetValue(
472472
| Ignore if null
473473
>-------------------------------------------------------------------------------------------------------------------------
474474
| ###NOTE JJC20200501: Null or empty values are treated as deletions, and are not persisted to the data store. With
475-
| existing values, these are written to ensure that the collection is marked as IsDirty, thus allowing previous values to
476-
| be overwritten. Non-existent values, however, should simply be ignored.
475+
| existing values, these are written to DeletedItems to ensure the collection is marked as IsDirty, thus allowing previous
476+
| values to be overwritten. Non-existent values should simply be ignored, however; we shouldn't delete what doesn't exist.
477477
\-----------------------------------------------------------------------------------------------------------------------*/
478478
else if (value is null || String.IsNullOrEmpty(value.ToString())) {
479+
return;
479480
}
480481

481482
/*------------------------------------------------------------------------------------------------------------------------
@@ -507,8 +508,8 @@ internal void SetValue(
507508
/*------------------------------------------------------------------------------------------------------------------------
508509
| Persist item to collection
509510
\-----------------------------------------------------------------------------------------------------------------------*/
510-
if (updatedItem is null) {
511-
return;
511+
if (updatedItem.Value is null) {
512+
Remove(key);
512513
}
513514
else if (originalItem is not null) {
514515
this[IndexOf(originalItem)] = updatedItem;
@@ -605,11 +606,15 @@ protected override void SetItem(int index, TItem item) {
605606
/// cref="TrackedRecord{T}"/>s are marked as <see cref="TrackedRecord{T}.IsDirty"/>.
606607
/// </remarks>
607608
protected override void RemoveItem(int index) {
608-
var trackedRecord = this[index];
609-
if (!AssociatedTopic.IsNew) {
610-
DeletedItems.Add(trackedRecord.Key);
609+
var trackedRecord = this[index] with {
610+
Value = null
611+
};
612+
if (_topicPropertyDispatcher.Enforce(trackedRecord.Key, trackedRecord)) {
613+
if (!AssociatedTopic.IsNew) {
614+
DeletedItems.Add(trackedRecord.Key);
615+
}
616+
base.RemoveItem(index);
611617
}
612-
base.RemoveItem(index);
613618
}
614619

615620
/*==========================================================================================================================
@@ -620,12 +625,15 @@ protected override void RemoveItem(int index) {
620625
/// it is appropriately marked as <see cref="IsDirty()"/>.
621626
/// </summary>
622627
/// <remarks>
623-
/// When an <see cref="TrackedRecord{T}"/> is removed, <see cref="IsDirty()"/> will return true—even if no remaining <see
624-
/// cref="TrackedRecord{T}"/>s are marked as <see cref="TrackedRecord{T}.IsDirty"/>.
628+
/// In order to ensure any business logic is enforced, <see cref="ClearItems()"/> loops through every <see cref="
629+
/// TrackedRecord{T}"/> in the <see cref="TrackedRecordCollection{TItem, TValue, TAttribute}"/> and explicitly calls <see
630+
/// cref="KeyedCollection{TKey, TItem}.Remove(TKey)"/>. This is slower, but ensures that any state tracking and null
631+
/// validation that occurs in the properties is maintained. Fortunately, this is a rare use case; we typically expect
632+
/// attributes to be handled individually.
625633
/// </remarks>
626634
protected override void ClearItems() {
627-
if (!AssociatedTopic.IsNew) {
628-
DeletedItems.AddRange(Items.Select(a => a.Key));
635+
foreach (var item in Items.ToList()) {
636+
Remove(item);
629637
}
630638
base.ClearItems();
631639
}

0 commit comments

Comments
 (0)