diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs
index 32b1abfb..aee11772 100644
--- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs
+++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs
@@ -1,16 +1,27 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System.Collections.ObjectModel;
using Crestron.SimplSharp;
namespace PepperDash.Essentials.Core;
///
-/// Basically a List , with an indexer to find feedbacks by key name
+/// Behaves like a List, with an indexer to find feedbacks by key name. Enforces unique feedback keys
+/// (case-insensitive) - attempting to add a feedback whose key already exists in the collection throws an
+/// . Feedbacks with no explicit key ( == "") are exempt
+/// from the uniqueness check, since many feedbacks are intentionally constructed without a key and are not meant
+/// to be looked up by the string indexer.
///
-public class FeedbackCollection : List where T : Feedback
+///
+/// Derives from rather than so key-uniqueness can be enforced
+/// via the virtual Insert/Set/Remove/Clear hooks (List{T}'s members are not virtual). Public surface area
+/// (int indexer, Add/Insert/Remove/Clear, collection-initializer support, enumeration) is preserved; an
+/// method is provided for source compatibility with existing List{T}.AddRange call sites.
+///
+public class FeedbackCollection : Collection where T : Feedback
{
+ private readonly Dictionary _feedbacksByKey = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
///
/// Case-insensitive port lookup linked to feedbacks' keys
///
@@ -18,7 +29,87 @@ public class FeedbackCollection : List where T : Feedback
{
get
{
- return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
+ return key != null && _feedbacksByKey.TryGetValue(key, out var feedback) ? feedback : null;
+ }
+ }
+
+ ///
+ /// Adds a range of feedbacks to the collection. Provided for source compatibility with List{T}.AddRange.
+ ///
+ /// The feedbacks to add.
+ public void AddRange(IEnumerable items)
+ {
+ if (items == null)
+ throw new ArgumentNullException(nameof(items));
+
+ foreach (var item in items)
+ {
+ Add(item);
+ }
+ }
+
+ ///
+ protected override void InsertItem(int index, T item)
+ {
+ ValidateNewItem(item);
+ base.InsertItem(index, item);
+ if (!string.IsNullOrEmpty(item.Key))
+ {
+ _feedbacksByKey[item.Key] = item;
+ }
+ }
+
+ ///
+ protected override void SetItem(int index, T item)
+ {
+ ValidateNewItem(item, indexBeingReplaced: index);
+ var replaced = Items[index];
+ base.SetItem(index, item);
+ if (replaced != null && !string.IsNullOrEmpty(replaced.Key) &&
+ !string.Equals(replaced.Key, item.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ _feedbacksByKey.Remove(replaced.Key);
+ }
+ if (!string.IsNullOrEmpty(item.Key))
+ {
+ _feedbacksByKey[item.Key] = item;
+ }
+ }
+
+ ///
+ protected override void RemoveItem(int index)
+ {
+ var removed = Items[index];
+ base.RemoveItem(index);
+ if (removed != null && !string.IsNullOrEmpty(removed.Key))
+ {
+ _feedbacksByKey.Remove(removed.Key);
+ }
+ }
+
+ ///
+ protected override void ClearItems()
+ {
+ base.ClearItems();
+ _feedbacksByKey.Clear();
+ }
+
+ private void ValidateNewItem(T item, int? indexBeingReplaced = null)
+ {
+ if (item == null)
+ throw new ArgumentNullException(nameof(item));
+
+ if (string.IsNullOrEmpty(item.Key))
+ return;
+
+ if (_feedbacksByKey.TryGetValue(item.Key, out var existing))
+ {
+ var existingIndex = Items.IndexOf(existing);
+ if (existingIndex != indexBeingReplaced)
+ {
+ throw new ArgumentException(
+ $"A Feedback with key '{item.Key}' already exists in this FeedbackCollection", nameof(item));
+ }
}
}
}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs
index 736d507f..d78021b4 100644
--- a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs
+++ b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs
@@ -1,22 +1,104 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Collections.ObjectModel;
namespace PepperDash.Essentials.Core;
///
- /// Represents a RoutingPortCollection, which is essentially a List with an indexer for case-insensitive lookup of ports by their key names.
+ /// Represents a RoutingPortCollection, which behaves like a List with an indexer for case-insensitive lookup of
+ /// ports by their key names. Enforces unique port keys (case-insensitive) - attempting to add a port whose key
+ /// already exists in the collection throws an .
///
- public class RoutingPortCollection : List where T: RoutingPort
+ ///
+ /// Derives from rather than so key-uniqueness can be enforced
+ /// via the virtual Insert/Set/Remove/Clear hooks (List{T}'s members are not virtual). Public surface area
+ /// (int indexer, Add/Insert/Remove/Clear, collection-initializer support, enumeration) is preserved; an
+ /// method is provided for source compatibility with existing List{T}.AddRange call sites.
+ ///
+ public class RoutingPortCollection : Collection where T: RoutingPort
{
+ private readonly Dictionary _portsByKey = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
///
/// Case-insensitive port lookup linked to ports' keys
///
- public T this[string key]
+ public T this[string key]
{
get
{
- return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
+ return key != null && _portsByKey.TryGetValue(key, out var port) ? port : null;
+ }
+ }
+
+ ///
+ /// Adds a range of ports to the collection. Provided for source compatibility with List{T}.AddRange.
+ ///
+ /// The ports to add.
+ public void AddRange(IEnumerable items)
+ {
+ if (items == null)
+ throw new ArgumentNullException(nameof(items));
+
+ foreach (var item in items)
+ {
+ Add(item);
+ }
+ }
+
+ ///
+ protected override void InsertItem(int index, T item)
+ {
+ ValidateNewItem(item);
+ base.InsertItem(index, item);
+ _portsByKey[item.Key] = item;
+ }
+
+ ///
+ protected override void SetItem(int index, T item)
+ {
+ ValidateNewItem(item, indexBeingReplaced: index);
+ var replaced = Items[index];
+ base.SetItem(index, item);
+ if (replaced != null && !string.Equals(replaced.Key, item.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ _portsByKey.Remove(replaced.Key);
+ }
+ _portsByKey[item.Key] = item;
+ }
+
+ ///
+ protected override void RemoveItem(int index)
+ {
+ var removed = Items[index];
+ base.RemoveItem(index);
+ if (removed != null)
+ {
+ _portsByKey.Remove(removed.Key);
+ }
+ }
+
+ ///
+ protected override void ClearItems()
+ {
+ base.ClearItems();
+ _portsByKey.Clear();
+ }
+
+ private void ValidateNewItem(T item, int? indexBeingReplaced = null)
+ {
+ if (item == null)
+ throw new ArgumentNullException(nameof(item));
+ if (string.IsNullOrEmpty(item.Key))
+ throw new ArgumentException("RoutingPort must have a non-empty Key", nameof(item));
+
+ if (_portsByKey.TryGetValue(item.Key, out var existing))
+ {
+ var existingIndex = Items.IndexOf(existing);
+ if (existingIndex != indexBeingReplaced)
+ {
+ throw new ArgumentException(
+ $"A RoutingPort with key '{item.Key}' already exists in this RoutingPortCollection", nameof(item));
+ }
}
}
}