Getting Started
This guide will help you install and start using Wolfgang.Extensions.ICollection in your .NET projects.
Installation
Using NuGet Package Manager (Visual Studio)
- Open your project in Visual Studio
- Right-click on your project in Solution Explorer
- Select "Manage NuGet Packages..."
- Search for "Wolfgang.Extensions.ICollection"
- Click "Install"
Using Package Manager Console
Install-Package Wolfgang.Extensions.ICollection
Using .NET CLI
dotnet add package Wolfgang.Extensions.ICollection
Manual Package Reference
Add this to your .csproj file:
<ItemGroup>
<PackageReference Include="Wolfgang.Extensions.ICollection" Version="x.x.x" />
</ItemGroup>
Basic Usage
1. Add the Using Directive
First, add the namespace to your C# file:
using Wolfgang.Extensions.ICollection;
2. Bulk Additions
AddRange works on any ICollection<T> — List<T>, HashSet<T>, LinkedList<T>, Collection<T>, ObservableCollection<T>, and any custom implementation:
ICollection<string> names = new List<string>();
names.AddRange(new[] { "Alice", "Bob", "Charlie" });
ICollection<int> numbers = new HashSet<int>();
numbers.AddRange(new[] { 1, 2, 3, 4, 5 }); // HashSet dedups → unique items only
ICollection<double> values = new LinkedList<double>();
values.AddRange(new[] { 1.5, 2.5, 3.5 });
For predicate-gated bulk add, use AddRangeIf:
ICollection<int> evens = new List<int>();
evens.AddRangeIf(Enumerable.Range(1, 20), n => n % 2 == 0); // → 2, 4, …, 20
For "add only if not already present", use AddIfNotContains:
ICollection<int> seen = new List<int> { 1, 2, 3 };
bool addedThree = seen.AddIfNotContains(3); // → false (already present)
bool addedFour = seen.AddIfNotContains(4); // → true; collection now { 1, 2, 3, 4 }
int addedMany = seen.AddIfNotContains(new[] { 4, 5, 6 }); // → 2 (only 5 and 6 were new)
HashSet<T> and SortedSet<T> get a single-lookup fast path that uses ISet<T>.Add directly (and honours the set's own IEqualityComparer<T>).
3. Bulk Removals
ICollection<int> nums = new List<int> { 1, 2, 3, 4, 5, 6 };
nums.RemoveRange(new[] { 1, 6 }); // → 2, 3, 4, 5
int removed = nums.RemoveWhere(n => n > 3); // → 2 (collection now { 2, 3 })
nums.ReplaceAll(new[] { 10, 20, 30 }); // → 10, 20, 30
RemoveWhere uses HashSet<T>.RemoveWhere as a fast path for HashSet<T> consumers; other implementations get a safe materialise-then-mutate pattern that never trips the BCL's mutate-during-enumerate contract.
4. Presence Checks
Use IsEmpty() and IsNotEmpty() for cleaner, more readable emptiness checks:
ICollection<string> names = new List<string>();
// Instead of: if (names.Count == 0)
if (names.IsEmpty())
{
Console.WriteLine("No names found.");
}
names.Add("Alice");
// Instead of: if (names.Count > 0)
if (names.IsNotEmpty())
{
Console.WriteLine($"Found {names.Count} name(s).");
}
Readability note:
IsEmpty()andIsNotEmpty()useICollection<T>.Countdirectly (typicallyO(1)for standardICollection<T>implementations), providing a clearer, self-documenting alternative to!Enumerable.Any().
Common Scenarios
Scenario 1: Adding Items from Multiple Sources
using Wolfgang.Extensions.ICollection;
public class DataAggregator
{
public ICollection<Product> GetAllProducts()
{
ICollection<Product> allProducts = new List<Product>();
// Add products from different sources
allProducts.AddRange(GetDatabaseProducts());
allProducts.AddRange(GetCachedProducts());
allProducts.AddRange(GetApiProducts());
return allProducts;
}
private IEnumerable<Product> GetDatabaseProducts() { /* ... */ }
private IEnumerable<Product> GetCachedProducts() { /* ... */ }
private IEnumerable<Product> GetApiProducts() { /* ... */ }
}
Scenario 2: Working with LINQ Results
using Wolfgang.Extensions.ICollection;
using System.Linq;
public class NumberProcessor
{
public ICollection<int> GetEvenNumbers(int max)
{
ICollection<int> result = new List<int>();
result.AddRangeIf(Enumerable.Range(1, max), n => n % 2 == 0);
return result;
}
}
Scenario 3: Building Collections Incrementally
using Wolfgang.Extensions.ICollection;
public class ReportBuilder
{
private ICollection<string> _reportLines = new List<string>();
public void AddHeader()
{
_reportLines.AddRange(new[]
{
"===================",
" Monthly Report ",
"==================="
});
}
public void AddSection(string title, IEnumerable<string> data)
{
_reportLines.AddRange(new[] { "", $"## {title}", "" });
_reportLines.AddRange(data);
}
public IEnumerable<string> GetReport() => _reportLines;
}
Scenario 4: Set-Aware Deduplication
using Wolfgang.Extensions.ICollection;
public class TagAggregator
{
public ICollection<string> CollectUniqueTags(IEnumerable<IEnumerable<string>> tagLists)
{
// HashSet<T> typed as ICollection<T> so the extension binds AND
// the ISet<T>.Add fast path kicks in for AddIfNotContains.
ICollection<string> unique = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var tags in tagLists)
{
unique.AddIfNotContains(tags);
}
return unique;
}
}
Scenario 5: Replacing the Contents of an Observable Collection
using System.Collections.ObjectModel;
using Wolfgang.Extensions.ICollection;
public class ViewModel
{
public ObservableCollection<string> Items { get; } = new();
public void Refresh(IEnumerable<string> newItems)
{
// ReplaceAll: clear + re-populate. The UI binding sees the full
// turnover (one Reset + one Add-per-item, or one Add range
// depending on the implementation).
ICollection<string> bindable = Items;
bindable.ReplaceAll(newItems);
}
}
Error Handling
All methods perform argument validation and surface the target collection's own contract:
using Wolfgang.Extensions.ICollection;
using System.Collections.ObjectModel;
// ArgumentNullException if source is null
ICollection<string>? nullCollection = null;
nullCollection!.AddRange(new[] { "item" }); // Throws ArgumentNullException("source")
// ArgumentNullException if items is null
ICollection<string> collection = new List<string>();
collection.AddRange(items: null!); // Throws ArgumentNullException("items")
// NotSupportedException if collection is read-only (ReadOnlyCollection<T>, T[], etc.)
ICollection<int> readOnly = new ReadOnlyCollection<int>(new[] { 1, 2, 3 });
readOnly.AddRange(new[] { 4, 5 }); // Throws NotSupportedException
ICollection<int> array = new int[5];
array.RemoveRange(new[] { 1 }); // Same — arrays are fixed-size
// ArgumentNullException if predicate is null (AddRangeIf / RemoveWhere)
collection.RemoveWhere(predicate: null!); // Throws ArgumentNullException("predicate")
Important Considerations
Thread Safety
The extension methods are not thread-safe. If you need to access collections from multiple threads:
using Wolfgang.Extensions.ICollection;
ICollection<int> collection = new List<int>();
var lockObject = new object();
lock (lockObject)
{
collection.AddRange(itemsFromThreadA);
}
lock (lockObject)
{
collection.AddRange(itemsFromThreadB);
}
Self-Aliasing
Passing the same collection as both source and items (e.g. list.AddRange(list)) is safe for every mutating method that takes an IEnumerable<T> — each method snapshots items via ReferenceEquals before mutating, so the call doesn't trip the BCL's mutate-during-enumerate contract:
list.AddRange(list)— appends a copy of the current contentslist.AddRangeIf(list, pred)— appends the items in the current contents that satisfy the predicatelist.RemoveRange(list)— empties the collectionlist.ReplaceAll(list)— no-op (snapshot is restored afterClear)
Performance
The methods include fast paths where it matters:
AddRangepre-allocatesList<T>.Capacitywhen the source exposesCount, avoiding the amortised-doubling growth pathRemoveWheredelegates toHashSet<T>.RemoveWhere(Predicate<T>)when the receiver is aHashSet<T>(skips the temp-list allocation)AddIfNotContains(T)delegates toISet<T>.Addwhen the receiver is anyISet<T>(single lookup instead ofContains+Add)
Otherwise the cost is the obvious shape: O(n) over the input, with each per-item cost matching the target collection's own Add / Remove / Contains.
Empty Collections
Adding an empty enumerable is safe and does nothing:
ICollection<string> collection = new List<string> { "existing" };
collection.AddRange(Enumerable.Empty<string>()); // No exception, collection unchanged
Next Steps
- API Documentation — Explore detailed API documentation
- Introduction — Learn more about the library's design
- GitHub Repository — View source code and examples
Getting Help
If you encounter issues or have questions:
- Check the GitHub Issues for existing discussions
- Review the API Documentation for detailed method information
- Open a new issue if you've found a bug or have a feature request