Group contact names by a normalized comparison key to find candidate duplicates. A shared name key is not proof that two records are the same person.
dotnet add package RecursiveAI.OneContactRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using RecursiveAI.OneContact; string[] names = ["Bob Smith", "Robert Smith"]; var parsed = names.Select(PersonName.Parse); foreach (var name in parsed .DistinctBy(name => name.Key)) Console.WriteLine(name.Canonical);
Bob SmithUnzip the example, then dotnet run.
Full program
All sample input is written directly in the program.
using RecursiveAI.OneContact; string[] inputs = ["Bob Smith", "Robert Smith", "Jane Smith"]; var groups = inputs.Select(PersonName.Parse) .GroupBy(name => name.Key, StringComparer.Ordinal); foreach (var group in groups) { Console.WriteLine($"Key: {group.Key}"); Console.WriteLine($"Keep: {group.First().Canonical}"); Console.WriteLine($"Matches: {group.Count()}"); }
dotnet run prints:
Key: robert smith Keep: Bob Smith Matches: 2 Key: jane smith Keep: Jane Smith Matches: 1
The comparison key maps the nickname “Bob” to its formal name “Robert”, which is why both names share robert smith; display names stay unchanged.
PersonName.Parse throws ContactParseException on rejected input; use TryParse before grouping a batch that contains untrusted or mixed-kind text.
Variations
Compare canonical equality with key equality:
using RecursiveAI.OneContact; var first = PersonName.Parse("Bob Smith"); var second = PersonName.Parse("Robert Smith"); Console.WriteLine(first == second); Console.WriteLine(Contacts.SameKey(first, second));
False True
Related recipes
The short answer
Parse each contact’s name with OneContact, then group or deduplicate by PersonName.Key. The example groups Bob Smith with Robert Smith, because the documented key rules map that nickname to its formal name. It keeps Jane Smith separate. The snippet at the top retains one name per key; the full program shows the group sizes before any records are discarded.
This page matches names attached to contacts. It does not implement a full contact-record merge policy. Review a candidate group against the fields your application trusts before deleting or combining records. Two different people can have exactly the same name, including exactly the same comparison key.
How do I remove duplicates from a list of objects in C#?
Choose the property that defines equality for the operation, then use DistinctBy for one representative or GroupBy to inspect every member. Here that property is the parsed name’s key. The LINQ operation performs grouping; OneContact supplies the name-aware comparison value.
The full program prints the first canonical name in each group and a count. That makes the decision visible: a group of two is a pair of matching names, not two independently verified copies of one person. In a real contact list, retain record identifiers and the original fields while reviewing the group. A key is useful as an index into candidates, not as a substitute for the source record.
Ordinary PersonName equality compares canonical forms ordinally. Key equality is explicit. The variation prints False for canonical equality and True for Contacts.SameKey, showing why deduplicating the value objects directly would not produce this example’s result. See the API notes for value and comparison conventions.
How do I remove duplicates by a property with LINQ?
Use the comparison key property, not a lowercased copy of the display name. OneContact’s person key rules explain the distinction: the key folds accents, removes punctuation, maps selected nicknames, reduces middle names to initials and combines family-name parts. Titles and suffixes are dropped.
Those transformations are intentionally lossy. Canonical stays faithful enough for display; Key sacrifices distinctions to bring likely variants together. Never write the key back into the contact’s name field. Keep both when your workflow needs display and lookup.
The published key version’s output is stable; a rule change ships as a new KeyVersion. If keys are persisted in a database, preserve which version generated them. Comparing values generated under different rule versions without an explicit migration can undermine the meaning of the index.
Should I use DistinctBy or GroupBy in C#?
Use DistinctBy when keeping one representative per key is already the agreed policy. Use GroupBy when the members need inspection or when the output must explain why records were proposed as duplicates. The latter is the safer starting point for a contact-cleanup screen because no member disappears before review.
Name matching and whole-record merging are different operations. Even the library’s extraction merge rules are contextual: the extraction notes say phone numbers never decide a record merge because a main line can be shared. Don’t extend a name-only example into an unconditional rule that matching phone numbers or similar spellings identify one person. Make that decision against the requirements of the data you actually hold.