How it works
Equality is not always the same as the default comparison of two strings. A format may define identifiers as case-insensitive while preserving their original spelling for display. In that case, a comparer is part of the identifier's contract. Passing it only to one operation can create a program in which duplicate detection says two codes are equal but a later lookup says they are different.
StringComparer supplies reusable equality and ordering policies. OrdinalIgnoreCase compares machine-oriented text without applying a language-specific collation. It does not trim whitespace or perform every possible Unicode normalization. Each additional transformation must be a deliberate format rule. Keep canonicalization, equality, and display decisions separate so that one convenience step does not silently change the identifier domain.
Walk through the example. Distinct receives the case-insensitive comparer and reduces three spellings to two logical codes. A dictionary constructed with the same comparer accepts A1 and can retrieve it through a1. GroupBy then uses that policy to count the two spellings of A1 together. The report sorts by key explicitly, leaving the identity policy and display order visible as separate arguments.
Watch the boundary. Calling ToLower without an explicit culture is not an equivalent replacement for choosing a comparer. It also allocates transformed text and can obscure the original spelling. Equally, applying OrdinalIgnoreCase to human names is not automatically the right product decision. Start with the specification of the identifier and use the same policy throughout its lifecycle.
Practice independently. Create a list of account codes with case variants and surrounding whitespace. Define whether whitespace is part of a code or is removed at ingestion. Build a unique-code report, a lookup map, and occurrence groups, then test that all three agree about identity. Add a display policy that retains the first accepted spelling without changing the logical equality rule.
A working example
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
string[] codes = { "A1", "a1", "B2" };
var comparer = StringComparer.OrdinalIgnoreCase;
Console.WriteLine(codes.Distinct(comparer).Count());
var names = new Dictionary<string, string>(comparer) { ["A1"] = "Ada" };
Console.WriteLine(names["a1"]);
foreach (var group in codes.GroupBy(x => x, comparer).OrderBy(g => g.Key, StringComparer.Ordinal))
Console.WriteLine($"{group.Key}:{group.Count()}");
Expected output
2 Ada A1:2 B2:1
The comparer belongs to the identifier contract, so use it across every collection operation.