HashSet and Sorted Collections — Uniqueness and Ordering

HashSet<T> is a collection that stores only unique values. It is backed by a hash table (like Dictionary, but with no values — just keys). The Add() method returns false if the item is already present, making duplicate prevention trivial. Set operations — union, intersection, difference — are built in. In ASP.NET Core, HashSet<T> is the right choice for permission sets, tag collections, and any scenario where you need fast “does this exist?” checks without duplicates.

HashSet Fundamentals

var tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

// Add — returns true if added, false if already present
bool added1 = tags.Add("csharp");   // true
bool added2 = tags.Add("dotnet");   // true
bool added3 = tags.Add("CSharp");   // false — already in set (case-insensitive)

Console.WriteLine(tags.Count);   // 2 — "csharp" and "dotnet"

// Contains — O(1) lookup (same speed as Dictionary)
bool hasCsharp = tags.Contains("CSHARP");   // true (case-insensitive comparer)
bool hasJava   = tags.Contains("java");     // false

// Remove
tags.Remove("dotnet");

// Initialiser syntax
var adminPermissions = new HashSet<string>
{
    "posts.read", "posts.write", "posts.delete",
    "users.read", "users.write", "users.delete"
};
Note: HashSet<T> uses the same hashing mechanism as Dictionary<TKey, TValue>. For your own types to work correctly in a HashSet (or as dictionary keys), the type must have consistent GetHashCode() and Equals() implementations. If two objects are equal (via Equals()), they must return the same hash code. Records automatically satisfy this requirement — their equality is value-based and hash codes are derived from all properties. For custom classes, either override both methods or implement IEqualityComparer<T>.
Tip: Use HashSet<T> for fast membership testing even if you never need set algebra. The classic pattern is: build a HashSet<int> of authorised user IDs, then check authorisedIds.Contains(requestUserId) in O(1) per request. The alternative — a List<int> with .Contains() — is O(n) and noticeably slower for large permission sets. When the only operations you need are “add” and “is it there?”, HashSet is the right tool.
Warning: HashSet<T> does not maintain insertion order — when you iterate a HashSet, elements come out in an unspecified order that may change between iterations. If you need both uniqueness AND ordering, use SortedSet<T> (sorted by natural order or a custom comparer) or build a HashSet for deduplication then sort the result. In ASP.NET Core, tag arrays returned to the client should generally be sorted for a consistent, predictable API response.

Set Operations

var userPerms   = new HashSet<string> { "posts.read", "posts.write", "comments.read" };
var adminPerms  = new HashSet<string> { "posts.read", "posts.write", "posts.delete", "users.manage" };

// UnionWith — adds all items from the other set
var combined = new HashSet<string>(userPerms);
combined.UnionWith(adminPerms);
// { "posts.read", "posts.write", "comments.read", "posts.delete", "users.manage" }

// IntersectWith — keeps only items in BOTH sets
var shared = new HashSet<string>(userPerms);
shared.IntersectWith(adminPerms);
// { "posts.read", "posts.write" }

// ExceptWith — removes items that are in the other set
var userOnly = new HashSet<string>(userPerms);
userOnly.ExceptWith(adminPerms);
// { "comments.read" }

// Non-mutating equivalents (LINQ)
var union     = userPerms.Union(adminPerms).ToHashSet();
var intersect = userPerms.Intersect(adminPerms).ToHashSet();
var except    = userPerms.Except(adminPerms).ToHashSet();

// Subset / superset checks
bool isSubset   = userPerms.IsSubsetOf(adminPerms);    // false
bool isSuperset = adminPerms.IsSupersetOf(userPerms);  // false

SortedSet and Sorted Collections

// SortedSet — unique items maintained in sorted order
var sortedTags = new SortedSet<string> { "dotnet", "angular", "csharp", "sql" };
// Iteration: angular, csharp, dotnet, sql (alphabetical)

// SortedDictionary — dictionary with keys sorted
var sortedMap = new SortedDictionary<string, int>
{
    ["zebra"] = 1, ["apple"] = 2, ["mango"] = 3
};
// Iteration keys: apple, mango, zebra

// SortedList vs SortedDictionary
// SortedList: compact memory, O(log n) insertion/deletion (uses binary search)
// SortedDictionary: O(log n) balanced BST, faster insertions into random positions

Common Mistakes

Mistake 1 — Using List.Contains() for frequent membership checks

❌ Wrong — O(n) per check; slow for large sets:

List<string> perms = GetPermissions();
if (perms.Contains("posts.delete")) { }  // O(n) every time

✅ Correct — convert to HashSet once, check many times in O(1):

HashSet<string> perms = GetPermissions().ToHashSet(StringComparer.OrdinalIgnoreCase);
if (perms.Contains("posts.delete")) { }  // O(1)

Mistake 2 — Expecting HashSet iteration order to be stable

❌ Wrong — relying on iteration order for display or API responses.

✅ Correct — sort the result: tags.OrderBy(t => t).ToList() before returning from an API endpoint.

🧠 Test Yourself

A user has a set of permissions from their role, plus additional permissions granted directly. You need the combined unique set. What HashSet operation achieves this most efficiently?